컨텐츠로 건너뛰기

Flutter Bloc 핵심 컨셉

Bloc Widgets

BlocBuilder

BlocBuilderBlocbuilder 함수 기능이 필요한 Flutter 위젯입니다. BlocBuilder는 새로운 state에 대한 응답으로 위젯 빌드를 처리합니다. BlocBuilderStreamBuilder와 매우 유사하지만 필요한 보일러플레이트 코드의 양을 줄이기 위해 더 간단한 API를 갖고 있습니다. builder 함수는 잠재적으로 여러 번 호출될 수 있으며 state에 응답하여 위젯을 반환하는 순수 함수여야 합니다.

Navigation, Dialog 표시 등과 같은 state 변경에 대한 응답으로 무엇이든 간에 “수행”하려면 BlocListener를 참조하세요.

만약 bloc 파라미터가 생략되면 BlocBuilderBlocProvider와 현재 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를 반환하면 builderstate와 함께 호출되고 위젯이 다시 빌드됩니다. buildWhen이 false를 반환하면 builderstate와 함께 호출되지 않으며 리빌드는 일어나지 않습니다.

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

BlocSelectorBlocBuilder와 유사하지만 개발자가 현재 bloc state에 따라 새 값을 선택하여 업데이트를 필터링할 수 있는 Flutter 위젯입니다. 선택한 값이 변경되지 않으면 불필요한 빌드가 방지됩니다. BlocSelectorbuilder를 다시 호출해야 하는지 여부를 정확하게 결정하려면 선택한 값을 변경할 수 없어야(immutable) 합니다.

만약 bloc 파라미터가 생략되면 BlocBuilderBlocProvider와 현재 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

BlocProviderBlocProvider.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가 즉시 실행되도록 하려면 lazyfalse로 설정하면 됩니다.

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 extensions
context.read<BlocA>();
// without extensions
BlocProvider.of<BlocA>(context);

MultiBlocProvider

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

BlocListener는 필수 BlocWidgetListener와 선택적 Bloc 파라미터를 사용하고 bloc의 state 변경에 대한 응답으로 listener를 호출하는 Flutter 위젯입니다. Navigation, Shackbar 표시, Dialog 표시 등과 같이 state 변경당 한 번 발생해야 하는 기능에 사용해야 합니다.

listenerBlocBuilderbuilder와 달리 각 state 변경 (초기 state를 포함하지 않음)에 대해 한 번만 호출되며 void 함수입니다.

만약 bloc 파라미터가 생략되면 BlocBuilderBlocProvider와 현재 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를 반환하면 listenerstate와 함께 호출됩니다. listenWhen이 false를 반환하면 listenerstate와 함께 호출되지 않습니다.

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

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

BlocConsumer는 새로운 state에 반응하기 위해 builderlistener를 노출합니다. BlocConsumer는 중첩된 BlocListenerBlocBuilder와 유사하지만, 필요한 보일러플레이트 코드의 양을 줄입니다. BlocConsumer는 UI를 다시 빌드하고 bloc의 상태 변경에 대한 다른 반응을 실행해야 하는 경우에만 사용해야 합니다. BlocConsumer는 필수 BlocWidgetBuilderBlocWidgetListener와 선택적인 bloc, BlocBuilderCondition, BlocListenerCondition 파라미터를 사용합니다.

만약 bloc 파라미터가 생략되면 BlocBuilderBlocProvider와 현재 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
},
);

선택적 파라미터인 listenWhenbuildWhen을 구현하면 listenerbuilder가 호출되는 시점을 더욱 세밀하게 제어할 수 있습니다. listenWhenbuildWhen은 각 bloc state 변경 시 호출됩니다. 이들은 각각 이전 state와 현재 state를 취하고 builder 및/또는 listener 함수가 호출되는지 여부를 결정하는 bool을 반환해야 합니다. BlocConsumer가 초기화되면 이전 stateblocstate로 초기화됩니다. listenWhenbuildWhen은 선택사항이며 구현되지 않은 경우 기본값은 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

RepositoryProviderRepositoryProvider.of<T>(context)를 통해 child에게 repository을 제공하는 Flutter 위젯입니다. 이는 repository의 단일 인스턴스가 하위 트리 내의 여러 위젯에 제공될 수 있도록 종속성 주입(DI) 위젯으로 사용됩니다. BlocProvider는 bloc을 제공하는 데 사용해야 하는 반면, RepositoryProvider는 repository를 제공하는 데에만 사용해야 합니다.

RepositoryProvider(
create: (context) => RepositoryA(),
child: ChildA(),
);

그런 다음 ChildA에서 다음을 사용하여 Repository 인스턴스를 찾을 수 있습니다:

// with extensions
context.read<RepositoryA>();
// without extensions
RepositoryProvider.of<RepositoryA>(context)

MultiRepositoryProvider

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 사용법

BlocProvider를 사용하여 CounterPageCounterBloc을 제공하고 BlocBuilder를 사용하여 state 변경에 대한 반응을 살펴보겠습니다.

counter_bloc.dart
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));
}
}
main.dart
void main() => runApp(CounterApp());
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => CounterBloc(),
child: CounterPage(),
),
);
}
}
counter_page.dart
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에 알려줄 뿐 입니다.

RepositoryProvider 사용법

flutter_weather 예시의 맥락에서 RepositoryProvider를 사용하는 방법을 살펴보겠습니다.

weather_repository.dart
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의 다양한 인스턴스를 주입할 수 있습니다.

main.dart
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 인스턴스를 제공할 수 있습니다.

app.dart
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를 노출합니다.

weather_page.dart
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에 주입할 수 있습니다.

Extension Methods

Dart 2.7에 도입된 Extension methods는 기존 라이브러리에 기능을 추가하는 방법입니다. 이번 섹션에서는 package:flutter_bloc에 포함된 확장 메서드와 이를 사용하는 방법을 살펴보겠습니다.

flutter_blocInheritedWidget의 사용을 단순화하는 package:provider 에 대한 종속성이 있습니다.

내부적으로, package:flutter_blocpackage:provider를 사용하여 BlocProvider, MultiBlocProvider, RepositoryProvider 그리고 MultiRepositoryProvider 위젯을 구현합니다. package:flutter_blocpackage:provider의 확장인 ReadContext, WatchContext 그리고 SelectContext를 export 합니다.

context.read

context.read<T>()T타입에 가장 가까운 상위 인스턴스를 조회하며 기능적으로 BlocProvider.of<T>(context)와 동일합니다. context.readonPressed 콜백 내에 event를 추가하기 위해 bloc 인스턴스를 검색하는 데 가장 일반적으로 사용됩니다.

사용법

DO 콜백에 event를 추가하려면 context.read를 사용하세요.

onPressed() {
context.read<CounterBloc>().add(CounterIncrementPressed()),
}

AVOID context.read를 사용하여 build 메서드 내에서 상태를 찾지 마세요.

@override
Widget build(BuildContext context) {
final state = context.read<MyBloc>().state;
return Text('$state');
}

위의 사용법은 bloc의 state가 변경되어도 Text 위젯이 다시 리빌드되지 않기 때문에 오류가 발생하기 쉽습니다.

context.watch

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를 제한하세요.

@override
Widget 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 Buildercontext.watchMultiBlocBuilder처럼 사용하세요.

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를 사용하지 마세요.

@override
Widget 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.select

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를 제한하세요.

@override
Widget 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를 사용하지 마세요.

@override
Widget 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),
),
);
}