Flutter Bloc 핵심 컨셉
BlocBuilder는 Bloc
및 builder
함수 기능이 필요한 Flutter 위젯입니다. BlocBuilder
는 새로운 state에 대한 응답으로 위젯 빌드를 처리합니다. BlocBuilder
는 StreamBuilder
와 매우 유사하지만 필요한 보일러플레이트 코드의 양을 줄이기 위해 더 간단한 API를 갖고 있습니다. builder
함수는 잠재적으로 여러 번 호출될 수 있으며 state에 응답하여 위젯을 반환하는 순수 함수여야 합니다.
Navigation, Dialog 표시 등과 같은 state 변경에 대한 응답으로 무엇이든 간에 “수행”하려면 BlocListener
를 참조하세요.
만약 bloc
파라미터가 생략되면 BlocBuilder
는 BlocProvider
와 현재 BuildContext
를 사용하여 자동으로 bloc을 조회합니다.
BlocBuilder<BlocA, BlocAState>( builder: (context, state) { // return widget here based on BlocA's state },);
단일 위젯으로 범위가 지정되고, 상위 BlocProvider
및 현재 BuildContext
를 통해 접근할 수 없는 bloc을 제공하려는 경우에만 bloc 파라미터를 지정하세요.
BlocBuilder<BlocA, BlocAState>( bloc: blocA, // provide the local bloc instance builder: (context, state) { // return widget here based on BlocA's state },);
builder
함수가 호출되는 시점을 세밀하게 제어하기 위해 선택적 buildWhen
파라미터가 제공됩니다. buildWhen
은 이전 bloc state와 현재 bloc state를 가져온 후 boolean을 반환합니다. buildWhen
이 true를 반환하면 builder
가 state
와 함께 호출되고 위젯이 다시 빌드됩니다. buildWhen
이 false를 반환하면 builder
는 state
와 함께 호출되지 않으며 리빌드는 일어나지 않습니다.
BlocBuilder<BlocA, BlocAState>( buildWhen: (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 },);
BlocSelector는 BlocBuilder
와 유사하지만 개발자가 현재 bloc state에 따라 새 값을 선택하여 업데이트를 필터링할 수 있는 Flutter 위젯입니다. 선택한 값이 변경되지 않으면 불필요한 빌드가 방지됩니다. BlocSelector
가 builder
를 다시 호출해야 하는지 여부를 정확하게 결정하려면 선택한 값을 변경할 수 없어야(immutable) 합니다.
만약 bloc
파라미터가 생략되면 BlocBuilder
는 BlocProvider
와 현재 BuildContext
를 사용하여 자동으로 bloc을 조회합니다.
BlocSelector<BlocA, BlocAState, SelectedState>( selector: (state) { // return selected state based on the provided state. }, builder: (context, state) { // return widget here based on the selected state. },);
BlocProvider는 BlocProvider.of<T>(context)
를 통해 child에게 bloc을 제공하는 Flutter 위젯입니다. 이는 bloc의 단일 인스턴스가 하위 트리 내의 여러 위젯에 제공될 수 있도록 종속성 주입(DI) 위젯으로 사용됩니다.
대부분의 경우, BlocProvider
를 사용하여 나머지 하위 트리에서 사용할 수 있는 새 bloc을 생성해야 합니다. 이 경우 BlocProvider
가 bloc 생성을 담당하기 때문에 자동으로 bloc을 close하는 것도 처리합니다.
BlocProvider( create: (BuildContext context) => BlocA(), child: ChildA(),);
기본적으로, BlocProvider
는 bloc을 lazy하게 생성합니다. 즉, BlocProvider.of<BlocA>(context)
을 통해 bloc을 조회할 때 create
가 실행된다는 의미입니다.
이 동작을 무시하고 create
가 즉시 실행되도록 하려면 lazy
를 false
로 설정하면 됩니다.
BlocProvider( lazy: false, create: (BuildContext context) => BlocA(), child: ChildA(),);
어떤 경우에는 BlocProvider
를 사용하여 위젯 트리의 새 부분에 기존 bloc을 제공할 수 잇습니다. 이는 기존 bloc을 새 route에서 사용할 수 있도록 해야 할 때 가장 일반적으로 사용됩니다. 이 경우 BlocProvider
는 bloc을 생성하지 않았으므로 자동으로 bloc을 close하지 않습니다.
BlocProvider.value( value: BlocProvider.of<BlocA>(context), child: ScreenA(),);
그런 다음 ChildA
또는 ScreenA
애서 다음을 사용하여 BlocA
를 찾을 수 있습니다:
// with extensionscontext.read<BlocA>();
// without extensionsBlocProvider.of<BlocA>(context);
MultiBlocProvider는 여러 BlocProvider
위젯을 하나로 병합하는 Flutter 위젯입니다. MultiBlocProvider
는 가독성을 향상시키고 여러 BlocProvider
를 중첩할 필요성을 제거합니다. MultiBlocProvider
를 사용하면 다음과 같던 코드를:
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는 필수 BlocWidgetListener
와 선택적 Bloc
파라미터를 사용하고 bloc의 state 변경에 대한 응답으로 listener
를 호출하는 Flutter 위젯입니다. Navigation, Shackbar
표시, Dialog
표시 등과 같이 state 변경당 한 번 발생해야 하는 기능에 사용해야 합니다.
listener
는 BlocBuilder
의 builder
와 달리 각 state 변경 (초기 state를 포함하지 않음)에 대해 한 번만 호출되며 void
함수입니다.
만약 bloc
파라미터가 생략되면 BlocBuilder
는 BlocProvider
와 현재 BuildContext
를 사용하여 자동으로 bloc을 조회합니다.
BlocListener<BlocA, BlocAState>( listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(),);
BlocProvider
및 현재 BuildContext
를 통해 접근할 수 없는 bloc을 제공하려는 경우에만 bloc 파라미터를 지정하세요.
BlocListener<BlocA, BlocAState>( bloc: blocA, listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(),);
listner
함수가 호출되는 시점을 세밀하게 제어하기 위해 선택적 listenWhen
파라미터가 제공됩니다. listenWhen
은 이전 bloc state와 현재 bloc state를 가져온 후 boolean을 반환합니다. listenWhen
이 true를 반환하면 listener
는 state
와 함께 호출됩니다. listenWhen
이 false를 반환하면 listener
는 state
와 함께 호출되지 않습니다.
BlocListener<BlocA, BlocAState>( listenWhen: (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: const SizedBox(),);
MultiBlocListener는 여러 BlocListener
위젯을 하나로 병합하는 Flutter 위젯입니다. MultiBlocListener
는 가독성을 향상시키고 여러 BlocListener
를 중첩할 필요성을 제거합니다. MultiBlocListener
를 사용하면 다음과 같던 코드를:
BlocListener<BlocA, BlocAState>( listener: (context, state) {}, child: BlocListener<BlocB, BlocBState>( listener: (context, state) {}, child: BlocListener<BlocC, BlocCState>( listener: (context, state) {}, child: ChildA(), ), ),);
다음과 같이 변경할 수 있습니다:
MultiBlocListener( listeners: [ BlocListener<BlocA, BlocAState>( listener: (context, state) {}, ), BlocListener<BlocB, BlocBState>( listener: (context, state) {}, ), BlocListener<BlocC, BlocCState>( listener: (context, state) {}, ), ], child: ChildA(),);
BlocConsumer는 새로운 state에 반응하기 위해 builder
와 listener
를 노출합니다. BlocConsumer
는 중첩된 BlocListener
및 BlocBuilder
와 유사하지만, 필요한 보일러플레이트 코드의 양을 줄입니다. BlocConsumer
는 UI를 다시 빌드하고 bloc
의 상태 변경에 대한 다른 반응을 실행해야 하는 경우에만 사용해야 합니다. BlocConsumer
는 필수 BlocWidgetBuilder
및 BlocWidgetListener
와 선택적인 bloc
, BlocBuilderCondition
, BlocListenerCondition
파라미터를 사용합니다.
만약 bloc
파라미터가 생략되면 BlocBuilder
는 BlocProvider
와 현재 BuildContext
를 사용하여 자동으로 bloc을 조회합니다.
BlocConsumer<BlocA, BlocAState>( listener: (context, state) { // do stuff here based on BlocA's state }, builder: (context, state) { // return widget here based on BlocA's state },);
선택적 파라미터인 listenWhen
및 buildWhen
을 구현하면 listener
및 builder
가 호출되는 시점을 더욱 세밀하게 제어할 수 있습니다. listenWhen
및 buildWhen
은 각 bloc
state
변경 시 호출됩니다. 이들은 각각 이전 state
와 현재 state
를 취하고 builder
및/또는 listener
함수가 호출되는지 여부를 결정하는 bool
을 반환해야 합니다. BlocConsumer
가 초기화되면 이전 state
는 bloc
의 state
로 초기화됩니다. listenWhen
및 buildWhen
은 선택사항이며 구현되지 않은 경우 기본값은 true
입니다.
BlocConsumer<BlocA, BlocAState>( listenWhen: (previous, current) { // return true/false to determine whether or not // to invoke listener with state }, listener: (context, state) { // do stuff here based on BlocA's state }, buildWhen: (previous, current) { // 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 },);
RepositoryProvider는 RepositoryProvider.of<T>(context)
를 통해 child에게 repository을 제공하는 Flutter 위젯입니다. 이는 repository의 단일 인스턴스가 하위 트리 내의 여러 위젯에 제공될 수 있도록 종속성 주입(DI) 위젯으로 사용됩니다. BlocProvider
는 bloc을 제공하는 데 사용해야 하는 반면, RepositoryProvider
는 repository를 제공하는 데에만 사용해야 합니다.
RepositoryProvider( create: (context) => RepositoryA(), child: ChildA(),);
그런 다음 ChildA
에서 다음을 사용하여 Repository
인스턴스를 찾을 수 있습니다:
// with extensionscontext.read<RepositoryA>();
// without extensionsRepositoryProvider.of<RepositoryA>(context)
MultiRepositoryProvider는 여러 RepositoryProvider
위젯을 하나로 병합하는 Flutter 위젯입니다. MultiRepositoryProvider
는 가독성을 향상시키고 여러 RepositoryProvider
를 중첩할 필요성을 제거합니다. MultiRepositoryProvider
를 사용하면 다음과 같던 코드를:
RepositoryProvider<RepositoryA>( create: (context) => RepositoryA(), child: RepositoryProvider<RepositoryB>( create: (context) => RepositoryB(), child: RepositoryProvider<RepositoryC>( create: (context) => RepositoryC(), child: ChildA(), ), ),);
다음과 같이 변경할 수 있습니다:
MultiRepositoryProvider( providers: [ RepositoryProvider<RepositoryA>( create: (context) => RepositoryA(), ), RepositoryProvider<RepositoryB>( create: (context) => RepositoryB(), ), RepositoryProvider<RepositoryC>( create: (context) => RepositoryC(), ), ], child: ChildA(),);
BlocProvider
를 사용하여 CounterPage
에 CounterBloc
을 제공하고 BlocBuilder
를 사용하여 state 변경에 대한 반응을 살펴보겠습니다.
sealed class CounterEvent {}final class CounterIncrementPressed extends CounterEvent {}final class CounterDecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<CounterIncrementPressed>((event, emit) => emit(state + 1)); on<CounterDecrementPressed>((event, emit) => emit(state - 1)); }}
void main() => runApp(CounterApp());
class CounterApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: BlocProvider( create: (_) => CounterBloc(), child: CounterPage(), ), ); }}
class CounterPage extends StatelessWidget { @override Widget build(BuildContext 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: () => context.read<CounterBloc>().add(CounterIncrementPressed()), ), ), Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.remove), onPressed: () => context.read<CounterBloc>().add(CounterDecrementPressed()), ), ), ], ), ); }}
이 시점에서 우리는 Presentation 레이어를 Business Logic 레이어에서 성공적으로 분리했습니다. CounterPage
위젯은 사용자가 버튼을 탭할 때 어떤 일이 발생하는지 전혀 모릅니다. 위젯은 단순히 사용자가 증가 또는 감소 버튼을 눌렀음을 CounterBloc
에 알려줄 뿐 입니다.
flutter_weather
예시의 맥락에서 RepositoryProvider
를 사용하는 방법을 살펴보겠습니다.
class WeatherRepository { WeatherRepository({ WeatherApiClient? weatherApiClient }) : _weatherApiClient = weatherApiClient ?? WeatherApiClient();
final WeatherApiClient _weatherApiClient;
Future<Weather> getWeather(String city) async { final location = await _weatherApiClient.locationSearch(city); final woeid = location.woeid; final weather = await _weatherApiClient.getWeather(woeid); return Weather( temperature: weather.theTemp, location: location.title, condition: weather.weatherStateAbbr.toCondition, ); }}
앱이 WeatherRepository
에 명시적으로 종속되어 있으므로 생성자를 통해 인스턴스를 주입합니다. 이를 통해 빌드 Flavor나 환경에 따라 WeatherRepository
의 다양한 인스턴스를 주입할 수 있습니다.
import 'package:flutter/material.dart';import 'package:flutter_weather/app.dart';import 'package:weather_repository/weather_repository.dart';
void main() { runApp(WeatherApp(weatherRepository: WeatherRepository()));}
우리 앱에는 하나의 Repository만 있으므로 RepositoryProvider.value
를 통해 이를 위젯 트리에 삽입합니다. Repository가 두 개 이상인 경우 MultiRepositoryProvider
를 사용하여 하위 트리에 여러 repository 인스턴스를 제공할 수 있습니다.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:weather_repository/weather_repository.dart';
class WeatherApp extends StatelessWidget { const WeatherApp({Key? key, required WeatherRepository weatherRepository}) : _weatherRepository = weatherRepository, super(key: key);
final WeatherRepository _weatherRepository;
@override Widget build(BuildContext context) { return RepositoryProvider.value( value: _weatherRepository, child: BlocProvider( create: (_) => ThemeCubit(), child: WeatherAppView(), ), ); }}
대부분의 경우, Root 앱 위젯은 RepositoryProvider
를 통해 하위 트리에 하나 이상의 repository를 노출합니다.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_weather/weather/weather.dart';import 'package:weather_repository/weather_repository.dart';
class WeatherPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (context) => WeatherCubit(context.read<WeatherRepository>()), child: WeatherView(), ); }}
이제 bloc을 인스턴스화 할 때, context.read
를 통해 repository의 인스턴스에 접근하고 생성자를 통해 repository를 bloc에 주입할 수 있습니다.
Dart 2.7에 도입된 Extension methods는 기존 라이브러리에 기능을 추가하는 방법입니다. 이번 섹션에서는 package:flutter_bloc
에 포함된 확장 메서드와 이를 사용하는 방법을 살펴보겠습니다.
flutter_bloc
은 InheritedWidget
의 사용을 단순화하는 package:provider에 대한 종속성이 있습니다.
내부적으로, package:flutter_bloc
은 package:provider
를 사용하여 BlocProvider
, MultiBlocProvider
, RepositoryProvider
그리고 MultiRepositoryProvider
위젯을 구현합니다. package:flutter_bloc
은 package:provider
의 확장인 ReadContext
, WatchContext
그리고 SelectContext
를 export 합니다.
context.read<T>()
는 T
타입에 가장 가까운 상위 인스턴스를 조회하며 기능적으로 BlocProvider.of<T>(context)
와 동일합니다. context.read
는 onPressed
콜백 내에 event를 추가하기 위해 bloc 인스턴스를 검색하는 데 가장 일반적으로 사용됩니다.
✅ DO 콜백에 event를 추가하려면 context.read
를 사용하세요.
onPressed() { context.read<CounterBloc>().add(CounterIncrementPressed()),}
❌ AVOID context.read
를 사용하여 build
메서드 내에서 상태를 찾지 마세요.
@overrideWidget build(BuildContext context) { final state = context.read<MyBloc>().state; return Text('$state');}
위의 사용법은 bloc의 state가 변경되어도 Text
위젯이 다시 리빌드되지 않기 때문에 오류가 발생하기 쉽습니다.
context.read<T>()
와 마찬가지로, context.watch<T>()
는 T
타입에 가장 가까운 상위 인스턴스를 조회하며 인스턴스의 변경 사항도 listen 합니다. 기능적으로는 BlocProvider.of<T>(context, listening: true)
와 동일합니다.
제공된 T
타입의 Object
가 변경되면 context.watch
는 위젯 리빌드를 촉발합니다.
✅ DO 명시적으로 리빌드 scope를 지정하려면 context.watch
대신 BlocBuilder
를 사용하세요.
Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder<MyBloc, MyState>( builder: (context, state) { // Whenever the state changes, only the Text is rebuilt. return Text(state.value); }, ), ), );}
또는, Builder
를 사용하여 리빌드 scope를 제한하세요.
@overrideWidget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever the state changes, only the Text is rebuilt. final state = context.watch<MyBloc>().state; return Text(state.value); }, ), ), );}
✅ DO Builder
와 context.watch
를 MultiBlocBuilder
처럼 사용하세요.
Builder( builder: (context) { final stateA = context.watch<BlocA>().state; final stateB = context.watch<BlocB>().state; final stateC = context.watch<BlocC>().state;
// return a Widget which depends on the state of BlocA, BlocB, and BlocC });
❌ AVOID build
메서드 내의 상위 위젯이 state에 의존하지 않는 경우 context.watch
를 사용하지 마세요.
@overrideWidget build(BuildContext context) { // Whenever the state changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final state = context.watch<MyBloc>().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), );}
context.watch<T>()
와 마찬가지로, context.select<T, R>(R function(T value))
는 T
타입에 가장 가까운 상위 인스턴스를 조회하며 인스턴스의 변경 사항도 listen 합니다. context.watch
와 달리 context.select
를 사용하면 state의 작은 부분(일부)에서의 변경 사항을 listen할 수 있습니다.
Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name);}
위의 내용은 ProfileBloc
state의 name
프로퍼티가 변경될 때만 위젯을 다시 빌드합니다.
✅ DO 명시적으로 리빌드 scope를 지정하려면 context.select
대신 BlocSelector
를 사용하세요.
Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector<ProfileBloc, ProfileState, String>( selector: (state) => state.name, builder: (context, name) { // Whenever the state.name changes, only the Text is rebuilt. return Text(name); }, ), ), );}
또는, Builder
를 사용하여 리빌드 scope를 제한하세요.
@overrideWidget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever state.name changes, only the Text is rebuilt. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), );}
❌ AVOID build
메서드 내의 상위 위젯이 state에 의존하지 않는 경우 context.select
를 사용하지 마세요.
@overrideWidget build(BuildContext context) { // Whenever the state.value changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), );}