Flutter 入门与实战(八十八):灵活易用的Bloc 界面构建组件 —— BlocBuilder
我们上一篇讲了 BlocProvider
的使用,感觉和 Provider
几乎是一样的,没什么新鲜感。在上一篇中有一个 BlocBuilder 倒是有点奇怪,我们回顾一下代码:
BlocBuilder<CounterCubit, int>( builder: (context, count) => Text( '$count', style: TextStyle( fontSize: 32, color: Colors.blue, ), ), 复制代码
这里面的 count
会自动跟随 BlocProvider
的状态对象变化,但是我们并没有看到绑定的动作,比如我们使用 Provider
是使用 context.watch
方法,但这里没有。这个是怎么回事呢?本篇我们就来介绍 BlocBuilder
的使用。
BlocBuilder 与状态对象的绑定
flutter_bloc 源码中的BlocBuilder的定义如下所示:
class BlocBuilder<B extends BlocBase<S>, S> extends BlocBuilderBase<B, S> { const BlocBuilder({ Key? key, required this.builder, B? bloc, BlocBuilderCondition<S>? buildWhen, }) : super(key: key, bloc: bloc, buildWhen: buildWhen); final BlocWidgetBuilder<S> builder; @override Widget build(BuildContext context, S state) => builder(context, state); } 复制代码
绑定状态对象有两种方式,在没有指定 bloc
参数的时候,它会通过 BlocProvider
和 context
自动向上寻找匹配的状态对象。这个代码在其父类BlocBuilderBase
(是一个 StatefulWidget
)的 State
对象中实现,实际上使用的还是 context.read
来完成的。
@override void initState() { super.initState(); _bloc = widget.bloc ?? context.read<B>(); _state = _bloc.state; } 复制代码
而如果指定了 bloc
参数,那么就使用指定的 bloc
对象,这样可以使用自有的 bloc
对象而无需 BlocProvider
提供。这个用法有点像GetX 的 GetBuilder
了。比如我们的计数器应用,可以简化为下面的形式。
class BlocBuilderDemoPage extends StatelessWidget { final counterCubit = CounterCubit(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Bloc 计数器'), ), body: Center( child: BlocBuilder<CounterCubit, int>( builder: (context, count) => Text( '$count', style: TextStyle( fontSize: 32, color: Colors.blue, ), ), bloc: counterCubit, ), ), floatingActionButton: FloatingActionButton( onPressed: () { counterCubit.increment(); }, tooltip: '点击增加', child: Icon(Icons.add), ), ); } } 复制代码
按条件刷新
BlocBuilder
还有一个参数 buildWhen
,这是一个返回bool
值的回调方法:
typedef BlocBuilderCondition<S> = bool Function(S previous, S current); 复制代码
也就是我们可以根据前后状态来决定是否要刷新界面。举个例子,比如我们刷新前后的数据一致,那就没必要重新刷新界面了。我们以之前写过的仿掘金个人主页来验证一下。这里我们为了完成网络请求业务,我们构建了四个状态:
enum LoadingStatus { loading, //加载 success, //加载成功 failed, //加载失败 } 复制代码
然后使用Bloc
的 event
模式,定义3个 Event
。
abstract class PersonalEvent {} // 获取数据事件 class FetchEvent extends PersonalEvent {} // 成功事件 class FetchSucessEvent extends PersonalEvent {} // 失败事件 class FetchFailedEvent extends PersonalEvent {} 复制代码
同时定义了一个响应的状态数据类,将个人信息对象和加载状态聚合在一起。
class PersonalResponse { PersonalEntity? personalProfile; LoadingStatus status = LoadingStatus.loading; PersonalResponse({this.personalProfile, required this.status}); } 复制代码
PersonalBloc
的代码实现如下,对应3个事件我们处理的方式如下:
FetchEvent
:请求网络数据;FetchSucessEvent
:加载成功后,用请求得到的个人信息对象和加载状态构建新的PersonalResponse
对象,使用emit
通知界面刷新;FetchFailedEvent
:加载失败,置空PersonalResponse
的个人信息对象,并且标记加载状态为失败。
class PersonalBloc extends Bloc<PersonalEvent, PersonalResponse> { final String userId; PersonalEntity? _personalProfile; PersonalBloc(PersonalResponse initial, {required this.userId}) : super(initial) { on<FetchEvent>((event, emit) { getPersonalProfile(userId); }); on<FetchSucessEvent>((event, emit) { emit(PersonalResponse( personalProfile: _personalProfile, status: LoadingStatus.success, )); }); on<FetchFailedEvent>((event, emit) { emit(PersonalResponse( personalProfile: null, status: LoadingStatus.failed, )); }); on<RefreshEvent>((event, emit) { getPersonalProfile(userId); }); add(FetchEvent()); } void getPersonalProfile(String userId) async { _personalProfile = await JuejinService().getPersonalProfile(userId); if (_personalProfile != null) { add(FetchSucessEvent()); } else { add(FetchFailedEvent()); } } } 复制代码
在构造函数中我们直接请求数据(也可以让界面控制)。页面的实现和之前 GetX
的类似(详见:Flutter 入门与实战(七十三):再仿掘金个人主页来看 GetX 和 Provider 之间的 PK),只是我们使用 BlocBuilder
来完成。代码如下:
class PersonalHomePage extends StatelessWidget { PersonalHomePage({Key? key}) : super(key: key); final personalBloc = PersonalBloc( PersonalResponse( personalProfile: null, status: LoadingStatus.loading, ), userId: '70787819648695'); @override Widget build(BuildContext context) { return BlocBuilder<PersonalBloc, PersonalResponse>( bloc: personalBloc, builder: (_, personalResponse) { print('build PersonalHomePage'); if (personalResponse.status == LoadingStatus.loading) { return Center( child: Text('加载中...'), ); } if (personalResponse.status == LoadingStatus.failed) { return Center( child: Text('请求失败'), ); } PersonalEntity personalProfile = personalResponse.personalProfile!; return Stack( children: [ CustomScrollView( slivers: [ _getBannerWithAvatar(context, personalProfile), _getPersonalProfile(personalProfile), _getPersonalStatistic(personalProfile), ], ), Positioned( top: 40, right: 10, child: IconButton( onPressed: () { personalBloc.add(FetchEvent()); }, icon: Icon( Icons.refresh, color: Colors.white, ), ), ), ], ); }, buildWhen: (previous, next) { if (previous.personalProfile == null || next.personalProfile == null) { return true; } return previous.personalProfile!.userId != next.personalProfile!.userId; }, ); } // 其他代码略 } 复制代码
这里我们加了一个刷新按钮,每次点击都会发起一个FetchEvent
来请求新的数据,并且在 BlocBuilder
的 builder
中使用 print
打印界面刷新信息。但是我们构建了一个 buildWhen
参数,只有当前后两次的用户 id 不一致时才刷新界面(实际也可以进行对象的比较,需要重载PersonalEntity
的==
和 hashCode
方法),以减少不必要的界面刷新。 之后我们把这行代码注释掉,然后直接返回 true,也就是每次都刷新。我们来看看两种效果的对比。
可以看到,使用条件判断后,点击刷新按钮不会刷新界面,这是因为我们用的 userId
都是同一个。而如果注释掉直接返回 true
之后,每次点击刷新按钮都会刷新。通过这种方式,我们可以在用户刷新但数据没发生变化的时候减少不必要的界面刷新。完整源码请到这里下载:BLoC 状态管理源码。
总结
从本篇可以看到,BlocBuilder
的使用还是挺简洁的,而Bloc
的 event
模式其实和Redux
的模式(可以参考:Flutter 入门与实战(五十九):手把手带你使用 Redux 的中间件实现异步状态管理)还挺相似的,都是用户行为触发事件,然后响应事件,在状态管理中返回一个新的数据对象来触发界面刷新。而 BlocBuilder
既可以配合 BlocProvider
在组件树中使用 Bloc
对象,也可以单独拥有自己的Bloc
对象。而使用 buildWhen
回调函数,可以通过前后状态数据的比对来决定是否要刷新界面,从而减少不必要的界面刷新。
作者:岛上码农
链接:https://juejin.cn/post/7014567132750938119