BLoC패턴에 대해 알아보자.
BLoC(Business Logic Component)는 파올로 소아레스와 콩 후이라는 개발자에 의해 디자인 되어 2018 DartConf에서 발표되었다. Flutter의 상태 관리를 제어하기 위해서 디자인 되었다. Flutter를 위해서 설계 되었지만, 디자인 패턴이기 때문에, 어떠한 프레임워크나 언어에서도 사용할 수 있다.
BLoC는 Presentaion Layer와 businessLogic을 분리하여 코드를 작성할 수 있게 해준다.
Bloc
은 State
(상태)를 보유하고 이 State
의 Stream
(스트림)을 제공한다. Widget은Stream
을 구독하고 있어 State
가 변경될 때마다 알림을 받을 수 있다. Bloc
은 Widget
에게 Event를 받는다. Event
가 Bloc으로 전송 되고 Bloc
이 transition을 적용하여 Event를 처리한다. transition
은 Event에 대한 응답으로 한 상태에서 다른 상태로 변경하는 역할을 한다. transition이 적용된 후 스트림에 알림이 전송되고 1분 정도 뒤 UI에 상태 변경 사항이 반영된다.
pubspec.yaml 변경
dependencies:
bloc: ^8.1.0
flutter:
sdk: flutter
flutter_bloc: ^8.1.1
dev_dependencies:
bloc_test: ^9.1.0
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
mocktail: ^0.3.0
very_good_analysis: ^3.0.1
모든 dependencies 설치
flutter packages get
├── lib
│ ├── app.dart
│ ├── counter
│ │ ├── counter.dart
│ │ ├── cubit
│ │ │ └── counter_cubit.dart
│ │ └── view
│ │ ├── counter_page.dart
│ │ └── counter_view.dart
│ ├── counter_observer.dart
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml
애플리케이션의 모든 상태 변경을 관찰하는 데 도움이 되는 생성 방법.
lib/counter_observer.dart
를 생성하여
import 'package:bloc/bloc.dart';
//BlocObserver 애플리케이션의 모든 상태 변화를 관찰하는데 도움이 된다.
class CounterObserver extends BlocObserver {
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
}
발생하는 모든 상태 변경을 확인하기 위해 onChange를 재정의한다.
main.dart
의 내용을 다음과 같이 변경한다.
import 'package:bloc/bloc.dart';
import 'package:flutter/widgets.dart';
import 'package:bloccounterex/app.dart';
import 'package:bloccounterex/counter_observer.dart';
void main() {
Bloc.observer = CounterObserver(); // 위에 생성해놓은 Observer를 Bloc observer로 초기화
runApp(const CounterApp());
}
lib/app.dart
를 생성하여 MaterialApp을 extends하는 CounterApp
클래스를 만든다.
개인적으로 해당 부분은 main과 묶어서 작성해도 되는 부분인 것 같다.
import 'package:flutter/material.dart';
import 'counter/view/counter_page.dart';
class CounterApp extends MaterialApp {
const CounterApp({super.key}) : super(home: const CounterPage());
}
lib/counter/view/counter_page.dart
를 생성한다.
CounterPage
위젯은 CounterCubit
을 작성하여 CounterView
에 제공하는 역할을 한다.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloccounterex/counter/counter.dart';
import 'counter_view.dart';
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
Widget build(BuildContext context) {
return BlocProvider( //상태 관리를 위한 BlocProvider
create: (_) => CounterCubit(), //CounterCubit 생성
child: const CounterView(), //상태 관리할 View
);
}
}
여기서 훨씬 더 테스트 가능하고 재사용 가능한 코드를 얻으려면
Cubit
의 생성과 소비를 분리하 것이 중요하다.
lib/counter/cubit/counter_cubit.dart
을 생성한다.
CounterCubit
클래스는 두 가지 메서드를 가진다.
import 'package:bloc/bloc.dart';
class CounterCubit extends Cubit<int> { //Cubit이 관리하는 상태 유형을 int로 선언
CounterCubit() : super(0);
void increment() => emit(state + 1); //현재 상태에 +1 하는 메서드
void decrement() => emit(state - 1);//현재 상태에 -1 하는 메서드
}
CounterCubit
이 관리하고 있는 상태 유형은 int
이고 초기 값은 0이다.
VSCode Extension 또는 IntelliJ Plugin을 사용하면 새로운 Cubit을 자동으로 생성해준다.
lib/counter/view/counter_view.dart
를 생성한다.
CounterView
는 현재 카운트를 렌더링하고 카운터를 증가/감소(increment/decrement
)하기 위해 두 개의 FloatingActionButton
을 렌더링하는 역할을 한다.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloccounterex/counter/counter.dart';
class CounterView extends StatelessWidget {
const CounterView({super.key});
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(title: const Text('Bloc Counter Example')),
body: Center(
child: BlocBuilder<CounterCubit, int>( // 상태 변화에 변경되어야 할 부분은 Text 하나이기에
builder: (context, state) { // Text 하나만 Wrapping. 상태가 변화할 때마다 업데이트
return Text('$state', style: textTheme.headline2); //State호출
},
),
),
floatingActionButton: Column( //+ - 버튼을 구현하기 위한 Column
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
key: const Key('counterView_increment_floatingActionButton'),
child: const Icon(Icons.add),
onPressed: () => context.read<CounterCubit>().increment(),//CounterCubit의 increment 함수 호출
),
const SizedBox(height: 8),
FloatingActionButton(
key: const Key('counterView_decrement_floatingActionButton'),
child: const Icon(Icons.remove),
onPressed: () => context.read<CounterCubit>().decrement(),//CounterCubit decrement 함수 호출
),
],
),
);
}
}
BlocBuilder
는 CounterCubit
의 상태가 변경될 때 마다 텍스트를 업데이트하기 위해 텍스트 위젯을 감싸고 있다. context.read<CounterCubit>
은 가장 가까운 CounterCubit
인스턴스를 조회하는 데 사용된다.
lib/counter/counter.dart
를 생성한다.
// 소스코드를 캡슐화
export 'cubit/counter_cubit.dart';
export 'view/counter_page.dart';
이 두 작업을 통해 counter.dart
만 import하여도 모든 관련 코드를 import할 수 있다.