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
、 BlocObserver
和BLoC
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的主要内容,如果未能解决你的问题,请参考以下文章