Flutter 入门与实战(八十八):灵活易用的Bloc 界面构建组件 —— BlocBuilder

2,474 阅读4分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动。

前言

我们上一篇讲了 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 来完成的。

@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,  //加载失败
}

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

buildWhen 效果

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

总结

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

我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章,提供体系化的 Flutter 学习文章。对应源码请看这里:Flutter 入门与实战专栏源码。如有问题可以加本人微信交流,微信号:island-coder

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!