写点什么

使用 BLoC 构建 Flutter 的页面实例

作者:岛上码农
  • 2022 年 7 月 02 日
  • 本文字数:3768 字

    阅读完需:约 12 分钟

使用BLoC 构建 Flutter的页面实例

前言

我们上一篇讲了 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参数的时候,它会通过 BlocProvidercontext自动向上寻找匹配的状态对象。这个代码在其父类BlocBuilderBase(是一个 StatefulWidget)的 State 对象中实现,实际上使用的还是 context.read 来完成的。


@overridevoid 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,  //加载失败}
复制代码


然后使用Blocevent 模式,定义 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 的类似(详见:再仿个人主页来看 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来请求新的数据,并且在 BlocBuilderbuilder 中使用 print 打印界面刷新信息。但是我们构建了一个 buildWhen 参数,只有当前后两次的用户 id 不一致时才刷新界面(实际也可以进行对象的比较,需要重载PersonalEntity==hashCode方法),以减少不必要的界面刷新。 之后我们把这行代码注释掉,然后直接返回 true,也就是每次都刷新。我们来看看两种效果的对比。



可以看到,使用条件判断后,点击刷新按钮不会刷新界面,这是因为我们用的 userId 都是同一个。而如果注释掉直接返回 true 之后,每次点击刷新按钮都会刷新。通过这种方式,我们可以在用户刷新但数据没发生变化的时候减少不必要的界面刷新。完整源码请到这里下载:BLoC 状态管理源码

总结

从本篇可以看到,BlocBuilder的使用还是挺简洁的,而Blocevent 模式其实和Redux的模式(可以参考:Flutter 使用 Redux 的中间件实现异步状态管理)还挺相似的,都是用户行为触发事件,然后响应事件,在状态管理中返回一个新的数据对象来触发界面刷新。而 BlocBuilder 既可以配合 BlocProvider 在组件树中使用 Bloc 对象,也可以单独拥有自己的Bloc对象。而使用 buildWhen 回调函数,可以通过前后状态数据的比对来决定是否要刷新界面,从而减少不必要的界面刷新。


发布于: 刚刚阅读数: 3
用户头像

岛上码农

关注

用代码连接孤岛,公众号@岛上码农 2022.03.03 加入

从南漂到北,从北漂到南的业余码农

评论

发布
暂无评论
使用BLoC 构建 Flutter的页面实例_flutter_岛上码农_InfoQ写作社区