Flutter 状态管理BLoC

Posted 一叶飘舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 状态管理BLoC相关的知识,希望对你有一定的参考价值。

前言

在 Flutter 的状态管理插件中,BLoC(Business Logic Component)非常受欢迎,事实上在 GitHub 上,BLoC 在众多的状态管理插件中的 Star 是最多的( 共7.8k,Provider 是3.9k,GetX 是4.6k)。这主要的原因是 BLoC 更多的是一种设计模式,按照这种设计模式可以转变为很多种状态管理实现。实际上在 pub 搜索 BLoC 会出现很多相关的插件,当然,官方的还是 bloc 和 flutter_bloc 组合。我们本篇先来介绍一下 BLoC 的基本概念。

image.png

BLoC 与 Stream

BLoC 依赖 Stream和 StreamController实现,组件通过Sinks发送更新状态的事件,然后再通过 Streams通知其他组件更新。事件处理和通知刷新的业务逻辑都是由 BLoC 完成,从而实现业务逻辑与 UI 层的分离(有点类似 Redux),并且逻辑部分可以复用和可以单独进行单元测试。

image.png

Flutter 中的 BLoC

bloc package是为了快速在 Flutter 或 Dart 中实现 BLoC 模式的插件,BLoC 官网提供了很多示例和详细的文档,有兴趣深入了解的可以上官网啃英文文档和浏览示例项目的代码。在 BLoC 有三个重要的概念,分别是 Cubit、 BlocObserverBLoC

Cubit

Cubit是管理状态数据的 BlocBase 子类,它可以管理任意类型的数据,包括基本类型到复杂对象。在Cubit调用 emit构建新的状态数据前需要给状态数据一个初始值。当状态数据发生改变的时候,会触发 Cubit的 onChange回调,如果出现错误则会触发 onError回调。

image.png

class CounterCubit extends Cubit<int> 
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);

  @override
  void onChange(Change<int> change) 
    super.onChange(change);
    print(change);
  

  @override
  void onError(Object error, StackTrace stackTrace) 
    print('$error, $stackTrace');
    super.onError(error, stackTrace);
  


UI界面可以通过调用 Cubit对外暴露的更新状态方法触发状态更新,而在 onChange中会得到更新前后的状态,从而可以触发界面刷新。

BlocObserver

BlocObserver 可以监听所有的 Cubit 的变化,从而使得我们可以同时监听多个 Cubit。例如运行下面的代码

class CounterCubit extends Cubit<int> 
  CounterCubit(initial = 0) : super(initial);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);


class MyBlocObserver extends BlocObserver 
  @override
  void onCreate(BlocBase bloc) 
    print('BloC Observer onCreate:  $bloc.state');
    super.onCreate(bloc);
  

  @override
  void onChange(BlocBase bloc, Change change) 
    print('BloC Observer onChange: $change');
    super.onChange(bloc, change);
  

  @override
  void onClose(BlocBase bloc) 
    print('BloC Observer onClose: $bloc.state');
    super.onClose(bloc);
  

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) 
    print('Bloc Observer onError: $error, $stackTrace');
    super.onError(bloc, error, stackTrace);
  

  @override
  void onEvent(Bloc bloc, Object? event) 
    print('Bloc Observer onEvent: $event, $bloc.state');
    super.onEvent(bloc, event);
  


void main() 
  Bloc.observer = MyBlocObserver();
  final cubit = CounterCubit();

  cubit.increment();
  print('after increment: $cubit.state');

  cubit.decrement();
  print('after decrement: $cubit.state');

  final anotherCubit = CounterCubit();
  anotherCubit.increment();

  cubit.close();
  anotherCubit.close();


控制台打印结果为:

BloC Observer onCreate:  0
BloC Observer onChange: Change  currentState: 0, nextState: 1 
BloC Observer onChange: Change  currentState: 1, nextState: 0 
BloC Observer onCreate:  10
BloC Observer onChange: Change  currentState: 10, nextState: 11 
BloC Observer onClose: 0
BloC Observer onClose: 11

Bloc

Bloc也是继承 BlocBase的类,但相比 Cubit来说更为高级一些。它使用的是 events而不是暴露的函数来更新状态。

在 Bloc内部,有一个onEvent方法,在这个方法里,通过 EventTransformer将 event转换为更新状态的方法来刷新状态数据。每个event都可以有对应的 EventHandler来处理该 event,完成后再通过 emit触发通知状态更新。当状态转变前会调用 onTransition,在这里会有当前的状态,触发更新的 event和下一个状态。

image.png

用 Bloc 改写之前的 CounterCubit形式如下,由于 Bloc也是 BlocBase的子类,因此也可以使用 Observer监测它的变化。

abstract class CounterEvent 

class IncrementEvent extends CounterEvent 

class DecrementEvent extends CounterEvent 

class CounterBloc extends Bloc<CounterEvent, int> 
  CounterBloc(int initialState) : super(initialState) 
    on<IncrementEvent>((event, emit) => emit(state + 1));
    on<DecrementEvent>((event, emit) => emit(state - 1));
  

  @override
  void onTransition(Transition<CounterEvent, int> transition) 
    print(
        'Current: $transition.currentState, Next: $transition.nextState, Event: $transition.event');
    super.onTransition(transition);
  


void main() 
  Bloc.observer = MyBlocObserver();
  
  final counterBloc = CounterBloc(5);
  counterBloc.add(IncrementEvent());
  counterBloc.add(DecrementEvent());

  counterBloc.close();


执行结果如下:

Current: 5, Next: 6, Event: Instance of 'IncrementEvent'
BloC Observer onChange: Change  currentState: 5, nextState: 6 
Current: 6, Next: 5, Event: Instance of 'DecrementEvent'
BloC Observer onChange: Change  currentState: 6, nextState: 5 
BloC Observer onClose: 5

总结

从 Bloc的设计来看,使用了函数(Cubit形式)和事件( Bloc形式)的方式来驱动状态数据更新,然后再通过emit通知状态更新。通过这种方式解耦 UI 界面和业务逻辑。可以看到,Bloc
本身的业务逻辑和界面完全无关,这使得我们可以直接编写测试代码,而无需依赖界面,如同本篇的 main方法中的代码其实就可以作为单元测试代码来验证业务逻辑是否正确。这使得 Bloc
构建的应用程序的可维护性会更好。


手撸一个BlocProvider

了解了 BLoC 的使用后,心里不禁有点痒痒,写了好几个状态管理插件了,能不能利用 BLoC自己也撸一个 Flutter 状态管理组件,用来构建基于 BLoC 的响应式Flutter页面。说干就干,我们给这个状态管理组件命名为 SimpleBlocProvider

SimpleBlocProvider
定义

对于这个简单的状态管理组件 SimpleBlocProvider,因为需要放置到组件树中,因此肯定是一个 Widget,由于内部还需要维护数据,因此我们使用 StatefulWidget。等等!不是不推荐使用 StatefulWidget吗?需要注意,如果这个组件自己维护自身状态,不影响外部组件那是没问题的。比如说,一个按钮点击后会有点击效果,这个其实也需要使用 StatefulWidget
实现,但是这个行为只会引起自身刷新,那是没问题的。

其次,我们要把原先 UI 组件的构建放到SimpleBlocProvider中来,那就需要定义一个 构建组件的builder 参数,所有原先 UI组件的构建都由这个 builder 来完成,同时这个 builder应该携带最新的状态数据,以便更新 UI 组件。而且,状态数据是确定不了类型的,因此这个 builder应该是返回 Widget的泛型函数,定义如下。

typedef StateBuilder<T> = Widget Function(T state);

比如显示计数器的 Text,我们可以这么写:

SimpleBlocProvider<int> (
  builder: (count) => Text('$count'),
)

光有 builder还不够,我们需要 Bloc 逻辑组件,以便从逻辑组件里获取最新的状态数据,因此需要将 Bloc 逻辑组件也作为参数给 SimpleBlocProvider。于是我们就得到SimpleBlocProvider的基本定义了。

class SimpleBlocProvider<T> extends StatefulWidget 
  final StateBuilder<T> builder;
  final BlocBase<T> bloc;
  const SimpleBlocProvider(
      Key? key, required this.builder, required this.bloc)
      : super(key: key);

  @override
  _SimpleBlocProviderState<T> createState() => _SimpleBlocProviderState<T>();


BLoC 刷新

要实现 BLoC 刷新,我们需要监听 BLoC 状态数据的变化,从上一篇我们知道BLoC 是于 Stream
实现的,对于 Stream,可以使用其 listen方法来监听 Stream流数据的变化。listen 方法定义如下:

 StreamSubscription<T> listen(void onData(T event)?,
      Function? onError, void onDone()?, bool? cancelOnError);

因此,我们可以在 listen的 onData中调用 setState就可以做到刷新界面了。我们组件销毁的时候需要取消监听,因此我们_SimpleBlocProviderState中定义一个属性_streamSubscription存储 listen方法的返回值,并在 dispose中取消监听。

_streamSubscription = widget.bloc.stream.listen((data) 
  setState(() 
    _state = data;
  );
);

//

@override
void dispose() 
  _streamSubscription.cancel();
  super.dispose();


接下来就比较简单了,在_SimpleBlocProviderState的 build方法中直接返回 builder携带状态数据_state构建组件即可。

@override
Widget build(BuildContext context) 
  return widget.builder(_state);


这样,只要 BLoC 的状态数据发生了改变,就会通过 listen监听更新SimpleBlocProvider_state,并刷新SimpleBlocProvider组件,从而更新了 builder构建的组件。完整代码如下:

typedef StateBuilder<T> = Widget Function(T state);

class SimpleBlocProvider<T> extends StatefulWidget 
  final StateBuilder<T> builder;
  final BlocBase<T> bloc;
  const SimpleBlocProvider(
      Key? key, required this.builder, required this.bloc)
      : super(key: key);

  @override
  _SimpleBlocProviderState<T> createState() => _SimpleBlocProviderState<T>();


class _SimpleBlocProviderState<T> extends State<SimpleBlocProvider<T>> 
  late T _state;
  late StreamSubscription<T> _streamSubscription;

  @override
  void initState() 
    _state = widget.bloc.state;
    super.initState();
    _streamSubscription = widget.bloc.stream.listen((data) 
      setState(() 
        _state = data;
      );
    );
  

  @override
  Widget build(BuildContext context) 
    return widget.builder(_state);
  

  @override
  void dispose() 
    _streamSubscription.cancel();
    super.dispose();
  


总共不到40行代码就搞定了

SimpleBlocProvider 应用

现在来看怎么用,我们先来一个计数器看看。

class CounterCubit extends Cubit<int> 
  CounterCubit(initial = 0) : super(initial);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);

  @override
  void onChange(Change<int> change) 
    super.onChange(change);
  


class SimpleBlocCounterPage extends StatelessWidget 
  final counter = CounterCubit();
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc 计数器'),
      ),
      body: Center(
        child: SimpleBlocProvider<int>(
          builder: (count) => Text(
            '$count',
            style: TextStyle(
              fontSize: 32,
              color: Colors.blue,
            ),
          ),
          bloc: counter,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () 
          counter.increment();
        ,
        tooltip: '点击增加',
        child: Icon(Icons.add),
      ),
    );
  


运行结果

是不是和我们之前在使用 MobX,GetX 的 GetBuilder很类似?再来看自定义类,来个简单的 Person类,然后用 Bloc 的 event模式试试。

class Person 
  final String name;
  final String gender;

  const Person(required this.name, required this.gender);


abstract class PersonEvent 

class UsingCnNameEvent extends PersonEvent 

class UsingEnNameEvent extends PersonEvent 

class PersonBloc extends Bloc<PersonEvent, Person> 
  PersonBloc(Person person) : super(person) 
    on<UsingCnNameEvent>(
        (event, emit) => emit(Person(name: '岛上码农', gender: '男')));
    on<UsingEnNameEvent>(
        (event, emit) => emit(Person(name: 'island-coder', gender: 'male')));
  


class SimpleBlocCounterPage extends StatelessWidget 
  final personBloc = PersonBloc(Person(name: '岛上码农', gender: '男'));
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc 事件'),
      ),
      body: Center(
        child: SimpleBlocProvider<Person>(
          builder: (person) => Text(
            '姓名:$person.name,性别:$person.gender',
            style: TextStyle(
              fontSize: 22,
              color: Colors.blue,
            ),
          ),
          bloc: personBloc,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () 
          personBloc.add(UsingEnNameEvent());
        ,
        tooltip: '点击增加',
        child: Icon(Icons.add),
      ),
    );
  


运行起来也是没问题的。

总结

本篇介绍了使用 BLoC 实现简单状态管理的 SimpleBLocProvider
,这个自定义的 BlocProvider不到40行,当然这个代码距离实际使用还有差距,但是对于了解一下这些第三方状态管理插件的实现机制还是有帮助的。接下来我们将使用官方的 flutter_bloc
插件来讲具体的应用实例。


使用BlocBuilder构建合理的异步请求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参数的时候,它会通过 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
的类似(详见:通过个人主页来看 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 状态管理源码 (https://gitee.com/island-coder/flutter-beginner/tree/master/state_management_bloc)。

总结

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

以上是关于Flutter 状态管理BLoC的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 状态管理(BloC):无状态与有状态小部件

Flutter 状态管理之Bloc上

Flutter 状态管理之Bloc上

Flutter 状态管理之Bloc上

Flutter BLoC:管理主要数据类型的状态

Flutter 状态管理BLoC