Flutter - BLoC 패턴 알아보기
Flutter - BLoC 알아보기
BLoC 패턴에 대하여 알아보도록 하겠습니다.
목차
BLoC 패턴이란?
BLoC (business Logic Component)는 파올로 소아레스와 콩 후이라는 개발자에 의해 디자인되었고
2018년 DartConf에서 발표되었습니다.
BLoC는 Presentation Layer와 business Logic을 분리하여 코드를 작성할 수 있도록 해줍니다.
BLoC는 스트림을 이용하여 만들어집니다.
위젯은 Sinks (입구)를 통하여 BLoc에 이벤트를 보냅니다.
BLoC객체는 위젯으로부터 이벤트를 전달받으면 필요한 Repository 등으로부터 데이터를 전달받아 business Logic을 처리합니다.
business Logic을 처리한 후, BLoC 객체를 구독 중인 UI 객체들에게 상태를 전달합니다
위에서 이야기한 대로 위젯은 BLoC객체의 stream을 구독하고 있어서 BLoC객체의 streams를 통해 결과를 전달받습니다.
BLoC객체가 상태가 변경되면 BLoC의 상태를 구독 중인 위젯들은 그 상태로 UI를 변경하는 것입니다.
BLoC의 특징
- UI에서 여러 BLoC이 존재할 수 있습니다.
- UI와 business로직을 분리하여 관리합니다.
- BLoC은 여러 UI에서 구독할 수 있습니다. (재사용이 가능합니다)
- BLoC만을 분리하여 테스트도가 가능합니다.
RxDart를 이용한 BLoC
Bloc과 Widget을 바인딩 하는 방법으로 Stream 을 사용할 수 있습니다.
이 예에서는 편의를 위해 RxDart를 사용하였습니다.
RxDart는이 Stream API 위에 기능을 여러 기능을 추가한 라이브러리 입니다.
예제 Counter with RxDart
RxDart를 이용하여 BLoC패턴을 구현한 예입니다.
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
CounterBloc _counterBloc = CounterBloc(initialCount: 0);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: _counterBloc.counterObservable,
builder: (context, AsyncSnapshot<int> snapshot) {
return Text('${snapshot.data}',
style: Theme.of(context).textTheme.display1);
},
)
],
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: () => _counterBloc.increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => _counterBloc.decrement(),
tooltip: 'Decrement',
child: Icon(Icons.remove),
),
],
));
}
@override
void dispose() {
_counterBloc.dispose();
super.dispose();
}
}
counter_bloc.dart
class CounterBloc {
int initialCount = 0;
BehaviorSubject<int> _subjectCounter;
CounterBloc({this.initialCount}) {
_subjectCounter = BehaviorSubject<int>.seeded(this.initialCount);
}
Observable<int> get counterObservable => _subjectCounter.stream;
void increment() {
_subjectCounter.sink.add(++initialCount);
}
void decrement() {
_subjectCounter.sink.add(--initialCount);
}
void dispose() {
_subjectCounter.close();
}
}
Flutter BLoC 라이브러리 소개
https://github.com/felangel/bloc
BLoC 디자인 패턴을 구현하는 데 도움이 되는 예측 가능한 상태 관리 라이브러리를 이용하여
조금 더 쉽게 구현해 볼 수도 있습니다.
동작 로직
- Presentation Component : Flutter에서 흔히 사용하는 Widget입니다 . Bloc에 Event를 전달하고, State를 수신합니다.
- Business Logic Component : Event를 전달받았을 때 비즈니스 로직을 수행하며, 로직 수행후 State를 전달합니다.
- BackEnd : repository 등등..
Flutter BLoC 라이브러리 클래스들
BlocBuilder
BlocBuilder는 새로운 State를 전달받았을 때 build를 호출하여 widget을 변경합니다.
StreamBuilder , 혹은 FutureBuilder와 유사합니다.
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
// return widget here based on BlocA's state
}
)
condition 옵션을 이용하여 이전 BLoC의 State와 현재 BLoC의 State를 취하고 bool을 반환합니다.
condition이 false를 반환하면 builder를 호출하지 않습니다.
BlocBuilder<BlocA, BlocAState>(
condition: (previousState, state) {
// return true/false to determine whether or not
// to rebuild the widget with state
},
builder: (context, state) {
// return widget here based on BlocA's state
}
)
BlocProvider
BlocProvider는 child에 Bloc을 제공하는 Flutter widget입니다.
BLoC의 단일 인스턴스가 서브 트리 내의 여러 위젯에 제공될 수 있도록 종속성 주입 (DI) 위젯으로 사용됩니다.
BlocProvider(
create: (BuildContext context) => BlocA(),
child: ChildA(),
);
서브 트리에서 BLoC을 참조하여 사용하고자 한다면 아래와 같이 사용 가능합니다.
BlocProvider.of<BlocA>(context)
MultiBlocProvider
MultiBlocProvider는 여러여러 BlocProvider 위젯을 하나로 병합하는 Flutter 위젯입니다.
BlocProvider를 중첩할 필요가 없습니다.
BlocProvider<BlocA>(
create: (BuildContext context) => BlocA(),
child: BlocProvider<BlocB>(
create: (BuildContext context) => BlocB(),
child: BlocProvider<BlocC>(
create: (BuildContext context) => BlocC(),
child: ChildA(),
)
)
)
에서
MultiBlocProvider(
providers: [
BlocProvider<BlocA>(
create: (BuildContext context) => BlocA(),
),
BlocProvider<BlocB>(
create: (BuildContext context) => BlocB(),
),
BlocProvider<BlocC>(
create: (BuildContext context) => BlocC(),
),
],
child: ChildA(),
)
으로
BlocListener
BlocListener는 해당 Bloc의 State가 변경되었을 때 호출되는 위젯입니다.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: Container(),
)
condition속성을 이용하여 제어도 가능합니다.
BlocListener<BlocA, BlocAState>(
condition: (previousState, state) {
// return true/false to determine whether or not
// to call listener with state
},
listener: (context, state) {
// do stuff here based on BlocA's state
}
child: Container(),
)
MultiBlocListener
MultiBlocListener는 여러 BlocListener 위젯을 하나로 병합하는 Flutter 위젯입니다.
MultiBlocListener(
listeners: [
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
),
BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
),
BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
),
],
child: ChildA(),
)
RepositoryProvider
RepositoryProvider 는 하위트리의 자식에게 repository를 제공하는 Flutter 위젯입니다.
즉 ,서브 트리의 단일 위젯을 여러 위젯에 제공할 수 있도록 종속성 주입 (DI) 위젯으로 사용됩니다.
BlocProvider블록을 제공하는 데 사용해야 하는 반면 RepositoryProvider리포지토리에만 사용해야 합니다.
RepositoryProvider(
builder: (context) => RepositoryA(),
child: ChildA(),
);
서브 트리에서 아래와 같이 Repository 인스턴스를 검색할 수 있습니다.
RepositoryProvider.of<RepositoryA>(context)
MultiRepositoryProvider
MultiRepositoryProvider는 여러 RepositoryProvider위젯을 하나로 병합하는 Flutter 위젯입니다
MultiRepositoryProvider(
providers: [
RepositoryProvider<RepositoryA>(
builder: (context) => RepositoryA(),
),
RepositoryProvider<RepositoryB>(
builder: (context) => RepositoryB(),
),
RepositoryProvider<RepositoryC>(
builder: (context) => RepositoryC(),
),
],
child: ChildA(),
)
예제 2 Counter with BLoC_Libaray
counter_bloc.dart
enum CounterEvent { increment, decrement }
class CounterBloc extends Bloc<CounterEvent, int> {
@override
int get initialState => 0;
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
yield state - 1;
break;
case CounterEvent.increment:
yield state + 1;
break;
}
}
}
counter_page.dart
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
counterBloc.add(CounterEvent.increment);
},
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () {
counterBloc.add(CounterEvent.decrement);
},
),
),
],
),
);
}
}
정리
장점
- 책임의 분리
- 테스트 가능성 증가( Controller 테스트 가능)
- 레이아웃 구성의 자유
- StreamBuilder, BlocBuilder 등을 통한 불필요한 setState()의 감소
단점
- 필요한 위젯은 모두 BLoC에 액세스 할 수 있어야 합니다.
- 엑세스 방법
- 글로벌 싱글 톤
- dart클래스에는 소멸자가 없기 때문에 리소스를 제대로 해제할 수 없습니다.
- 권장되지 않는 방법
- 로컬 인스턴스
- 조상에 의한 Provided ( Bloc library의 방법)
- 글로벌 싱글 톤
Reference
https://github.com/felangel/bloc
https://bloclibrary.dev/#/gettingstarted
-