Flutter Bloc 核心概念
BlocBuilder 是一个 Flutter 的 Widget,它需要一个 Bloc
和一个 builder
函数。BlocBuilder
处理于构建响应新状态时构建的 Widget 。BlocBuilder
与 StreamBuilder
非常相似,但是 StreamBuilder
具有更简单的 API,以减少所需的样板代码量。builder
函数可能会被多次调用,并且必须是一个根据状态返回一个widget的纯函数。
如果你希望在状态变化时执行一些操作,比如导航、显示对话框等…,请查看 BlocListener
在BlocBuilder中,如果省略了 bloc
参数,则 BlocBuilder
会自动通过 BlocProvider
和当前的 BuildContext
进行查找。
BlocBuilder<BlocA, BlocAState>( builder: (context, state) { // return widget here based on BlocA's state },);
只有在希望提供一个仅作用于单个 widget 且无法透过父级 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 },);
在使用 BlocBuilder 时,如果要达到更细緻地控制 widget 的更新,可以使用 buildWhen
参数来实现何时调用 builder
函数。buildWhen
接受前一个 bloc 状态和当前 bloc 状态,并返回一个布尔值。如果 buildWhen
返回 true,则 builder
函数将被调用并使用当前 state
进行widget重建。如果 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 是一个 Flutter Widget,类似于 BlocBuilder,但允许开发者通过根据当前 bloc 状态选择新值来过滤更新。如果选择的值不变,则防止不必要的构建。选择的值必须是 immutable 的,以便 BlocSelector 可以准确地确定是否应该再次调用 builder。
如果bloc
被省略没有传入, BlocSelector
将自动使用 BlocProvider
和当前的 BuildContext
执行查找。
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 是一个 Flutter widget ,透过 BlocProvider.of<T>(context)
将一个 bloc 提供给它的children。BlocProvider
用作于依赖注入(DI)widget ,以便在子树 (subtree) 中可以提供单个 bloc 实例给多个widget 使用。
通常情况下,应该使用 BlocProvider
来创建新的 bloc,这样可以使该 bloc 对于子树中的其他 widget 使用。在这种情况下,由于 BlocProvider
负责创建 bloc,它将自动处理关闭该 bloc。
BlocProvider( create: (BuildContext context) => BlocA(), child: ChildA(),);
默认情况下,BlocProvider
将延迟创建 bloc 实例,这意味着当通过 BlocProvider.of<BlocA>(context)
查找该 bloc 时,create
方法才会被执行。
为了复盖这种行为并强制立即运行 create
方法,可以将 lazy
设置为 false
。
BlocProvider( lazy: false, create: (BuildContext context) => BlocA(), child: ChildA(),);
在某些情况下,可以使用 BlocProvider
来将一个已存在的 Bloc 提供给 Widget 树中的新部分。这通常发生在需要将一个已存在的 Bloc 提供给新的路由(route)时。在这种情况下,BlocProvider
不会自动关闭 Bloc,因为它并非 Bloc 的创建者。
BlocProvider.value( value: BlocProvider.of<BlocA>(context), child: ScreenA(),);
如此,从 ChildA
或者 ScreenA
中,我们可以获取 BlocA
// with extensionscontext.read<BlocA>();
// without extensionsBlocProvider.of<BlocA>(context);
MultiBlocProvider 是一个 Flutter Widget 用来将多个 BlocProvider
Widgets 合併为一个。
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 是一个 Flutter Widget,它接受一个 BlocWidgetListener
和一个可选的 Bloc
,并在 bloc 状态发生变化时调用 listener
。它应该用于需要每个状态变化仅执行一次的功能,例如导航、显示 SnackBar
、显示 Dialog
等等。
与 BlocBuilder
中的 builder
不同的是,listener
对于每个状态变化(初始状态除外)只会被调用一次,并且是一个 void
函数。
如果bloc
被省略没有传入, BlocListener
将自动使用 BlocProvider
和当前的 BuildContext
执行查找。
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(),);
要对 listener
函数何时调用进行更细緻地控制,可以提供一个可选的 listenWhen
。listenWhen
接受先前的 bloc 状态和当前的 bloc 状态,并返回一个布尔值。如果 listenWhen
返回 true,则会用 state
调用 listener
。如果 listenWhen
返回 false,则不会用 state
调用 listener
。
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 是 Flutter 中的一个 Widget,用来将多个 BlocListener
Widgets 合併为一个。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 提供了 builder
和 listener
,以便对新的状态做出反应。BlocConsumer
类似于嵌套的 BlocListener
和 BlocBuilder
,但可以减少所需的样板代码量。只有在需要重建 UI 并执行其他对 bloc
状态变化做出反应时,才应该使用 BlocConsumer
。BlocConsumer
接受BlocWidgetBuilder
(必传参数) 和 BlocWidgetListener
(必传参数) ,以及 bloc
(可选参数)、BlocBuilderCondition
(可选参数) 和 BlocListenerCondition
(可选参数)。
如果bloc
被省略没有传入, BlocConsumer
将自动使用 BlocProvider
和当前的 BuildContext
进行查找。
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
函数。当 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 },);
MultiBlocListener 是 Flutter 中的一个 Widget,透过 RepositoryProvider.of<T>(context)
将 repository 提供给其子节点。它被用作依赖注入(DI)小部件,以便在子树中提供一个仓库的单个实例给多个 Widgets。BlocProvider
应该用于提供 bloc,而 RepositoryProvider
应该只用于提供 repositories。
RepositoryProvider( create: (context) => RepositoryA(), child: ChildA(),);
那么从 ChildA
,我们可以通过以下代码获取 Repository
实例:
// with extensionscontext.read<RepositoryA>();
// without extensionsRepositoryProvider.of<RepositoryA>(context)
MultiRepositoryProvider 是一个 Flutter Widget 用来将多个 RepositoryProvider
Widgets 合併为一个。
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
来提供一个 CounterBloc
给一个 CounterPage
并且使用BlocBuilder
来对状态变化做出反应.
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()), ), ), ], ), ); }}
到目前为止,我们成功地将我们的表示层 ( presentational layer ) 与我们的业务逻辑层 (business logic layer) 分开了。注意,CounterPage
widget 不知道当用户点击按钮时会发生什么。该 widget 只是告诉 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
,我们通过建构函式注入一个实例。这使我们能够根据构建版本或环境注入不同的 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
将其注入到我们的 widget tree 中。如果您有多个 repository,则可以使用 MultiRepositoryProvider
将多个 repository 实例提供给 subtree。
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(), ), ); }}
在大多数情况下,应用程序的顶层 widgets 将透过 RepositoryProvider
向 subtree 公开一个或多个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
访问存储库的实例,并通过构造函数将存储库注入到 bloc 中。
Extension methods,在 Dart 2.7 中引入,是一种向现有库添加功能的方法。在本节中,我们将看一下 package:flutter_bloc
中包含的扩展方法以及它们的使用方式。
flutter_bloc
依赖于 package:provider,它简化了对 InheritedWidget
的使用。
在内部,package:flutter_bloc
使用 package:provider
实现了 BlocProvider
、MultiBlocProvider
、RepositoryProvider
和 MultiRepositoryProvider
widgets。 package:flutter_bloc
从 package:provider
中导出了 ReadContext
、WatchContext
和 SelectContext
扩展。
context.read<T>()
查找最接近的类型为 T 的祖先实例,并在功能上等同于 BlocProvider.of<T>(context)
。context.read
最常用于在 onPressed
回调中查找 bloc 实例以添加事件。
✅ 建议 在回调函数中使用context.read
方法来添加事件。
onPressed() { context.read<CounterBloc>().add(CounterIncrementPressed()),}
❌ 避免 在 build
方法当中使用context.read
查找状态
@overrideWidget build(BuildContext context) { final state = context.read<MyBloc>().state; return Text('$state');}
以上的使用可能会导致UI 不会反应最新的状态变化的错误,因为即使状态改变了 Text
widget 并不会被重新建立(rebuild),。
如同 context.watch<T>()
, context.watch<T>()
提供最接近祖先类型为 T
的实例,并且同时还会监听该实例的变化。它的功能等同于 BlocProvider.of<T>(context, listen: true)
。
如果提供类型 T 的对象发生变化,context.watch 将会触发重新构建(rebuild)。
✅ 建议 使用 BlocBuilder
而不是 context.watch
来明确地范围化重新构建。
Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder<MyBloc, MyState>( builder: (context, state) { // 当状态变化时,只会重新构建 Text。 return Text(state.value); }, ), ), );}
作为选择,建议使用 Builder
来范围化(或限定)重新构建的范围。
@overrideWidget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // 当状态变化时,只会重新构建 Text。 final state = context.watch<MyBloc>().state; return Text(state.value); }, ), ), );}
✅ 建议 将 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;
// 返回一个依赖于 BlocA 、BlocB, 和 BlocC 状态的 Widget。 });
❌ 避免 使用 context.watch
当父 Widget 的 build 方法不依赖于状态
@overrideWidget build(BuildContext context) { // 无论状态如何变化,都会重新构建 MaterialApp, // 即使它只用于 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
的实例 且同时监听该实例的变化。
不同于 context.watch
的是,context.select
允许你监听状态对象中的特定部分
Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name);}
以上实例将在属性 ProfileBloc
内中的 name
属性变化时重新构建 Widget。
✅ 建议 使用 BlocSelector 而不是 context.select,以明确地限定重新构建的范围。
Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector<ProfileBloc, ProfileState, String>( selector: (state) => state.name, builder: (context, name) { // 当 state.name 变化时,只有 Text 会重新构建。 return Text(name); }, ), ), );}
作为选择,建议使用 Builder
来范围化(或限定)重新构建的范围。
@overrideWidget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // 当 state.name 变化时,只有 Text 会重新构建。 final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), );}
❌ 避免 在父 Widget 的 build 方法中使用 context.select,但父 Widget 不依赖于状态。
@overrideWidget build(BuildContext context) { // 当 state.value 变化时,即使它只在 Text widget 中使用,也会重新构建 MaterialApp。 final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), );}