Flutter状态管理
flutter的状态管理方案很多,这里对相关知识做一下梳理。
传递State
Flutter将组件分为StatefulWidget,StatelessWidget,有状态的组件通常继承自StatefulWidget,通过State来管理自身状态。
当业务比较简单时,基于StatefulWidget就可以管理好整个应用的状态了。
但是当业务复杂起来时,会存在一些问题:
如图,有两个根节点需要共享某个状态,那么就需要把这个状态存到它们共同的父节点,然后逐级传递下来,当状态改变时,共同父节点之下的整个子树都会rebuild,而其中大部分组件的rebuild都是多余的,导致性能变差。
另外,这种方式可能会使父组件的state变得臃肿,有些数据可能并不适合放到它的state里但是为了共享只能放那儿。
因此,我们需要更进一步的手段进行状态管理。
多说两句,有部分客户端开发在接触Flutter时并不是很接受类React的这种范式,会想着把状态丢在某个管理器单例中,创建页面/Widget时去取用,如果状态改变时需要页面刷新,就抛个通知或者类似的方式让组件去更新。
在Flutter的世界里,把某个/某些全局状态搞成单例不是不可以,但通常仍倾向于保留响应式的优点(Flutter本身不算完全的响应式,因为setState是显式的,但至少有部分响应式的特点),组件在使用状态时维护监听关系,状态改变时通知订阅者。这种通知方式是像setState一样自然甚至是没有这种显式地发出通知步骤的,组件在读状态时就产生了订阅关系,而不是显式地订阅通知。总而言之,前面提到的想法太命令式了。
InheritedWidget
官方提供的共享数据基本方案。
InheritedWidget是一种特殊的功能性组件,它提供了一种将状态从上向下传递的方式。
比如Material组件中Theme的管理就使用了这种方式,在一个MaterialApp中,我们可以在build任意widget时使用ThemeData data = Theme.of(context)
来获取当前主题数据,同时也会使当前Widget依赖了Theme(准确地说是_InheritedTheme
这一组件),当主题变化时,所有依赖它的Widget都会被更新。这就解决了直接传递State导致多余的组件更新的问题。
看个demo,定义一个ShareDataWidget继承自InheritedWidget:
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({
@required this.data,
Widget child
}) :super(child: child);
final int data;
static ShareDataWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ShareDataWidget);
}
@override
bool updateShouldNotify(ShareDataWidget old) {
return old.data != data;
}
}
在父组件中:
class _InheritedWidgetTestRouteState extends State {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: ShareDataWidget( //使用ShareDataWidget
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: _TestWidget(),//子widget中依赖ShareDataWidget
),
RaisedButton(
child: Text("Increment"),
onPressed: () => setState(() => ++count),
)
],
),
),
);
}
}
子组件获取InheritedWidget中的数据:
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
return Text(ShareDataWidget
.of(context)
.data
.toString());
}
可以看到,数据还是父组件的state管理的,InheritedWidget只是对数据包装了一下,提供了一个通道供子组件取数据。
这里比较关键的一点是inheritFromWidgetOfExactType的实现,这决定了子组件寻找共享的Model的性能,看一下实现:
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}
_inheritedWidgets是个Map,dart中的Map默认是LinkedHashMap,这里的查找操作是O(1)的。因此性能是有保障的。
InheritedWidget缺点还是比较明显,一方面直接使用时代码上比较啰嗦,另一方面只提供了从上到下的数据传递,子组件想要改数据还需要使用Notification机制,就更重了。
由于InheritedWidget性能好但使用不便,社区在此基础上进行了很多封装。
ScopedModel
ScopedModel是早期Flutter社区封装的比较成功的状态管理组件。它把状态封装入Model里,修改数据的逻辑也在model里,通过跟InheritedWidget类似的方式把Model传给子组件,子组件可以从Model取数据,也可以直接调用Model的方法修改数据。
不过后来Flutter官方选择了更优秀且同样方便的状态管理框架provider进行推广,这里就不多介绍ScopedModel了。
provider
provider目前是官方钦定的应用状态管理组件。
Pragmatic State Management in Flutter (Google I/O'19)
看个demo
class CounterModel with ChangeNotifier {
int _count = 0;
int get value => _count;
void increment() {
_count++;
notifyListeners();
}
}
void main() {
final counter = CounterModel();
runApp(
ChangeNotifierProvider.value(
value: counter,
child: MaterialApp(
home:FirstScreen()
),
),
);
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _counter = Provider.of(context);
final textSize = Provider.of(context).toDouble();
return Scaffold(
appBar: AppBar(
title: Text('FirstPage'),
),
body: Center(
child: Text(
'Value: ${_counter.value}',
style: TextStyle(fontSize: textSize),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _counter.increment,
child: Icon(Icons.navigate_next),
),
);
}
}
定义一个CounterModel,通过Provider组件注入组件树,子组件在build时可以通过Provider.of
获取model,可以从model中获取数据,也可以直接调用model的方法(CounterModel.increment)修改数据。
可以看到这样做比InheritedWidget方便太多了。
这里用到的是ChangeNotifierProvider,跟ScopedModel用起来是基本一样的,它也是我们最常用的Provider。但它比ScopedModel优秀的地方在于,使用ScopedModel时必须继承它的Model才能用,因此ScopedModel是有比较强的侵入性的,而Provider就好很多。
除了ChangeNotifierProvider之外,Provider还提供了另外几种方式进行数据管理:
-
Provider
- 单纯共享数据给子组件,但是数据更新时不会通知子组件。
-
- 比起ChangeNotifierProvider区别主要是不会自动调用
ChangeNotifier.dispose
释放资源。一般不用。
- 比起ChangeNotifierProvider区别主要是不会自动调用
-
- 可以认为是ChangeNotifierProvider的特例,只监听一个数据的时候,使用ValueListenableProvider在修改数据时可以不用调用
notifyListeners()
。
- 可以认为是ChangeNotifierProvider的特例,只监听一个数据的时候,使用ValueListenableProvider在修改数据时可以不用调用
-
- 用于监听一个Stream
-
- 提供了一个 Future 给其子孙节点,并在 Future 完成时,通知依赖的子孙节点进行刷新
Redux(flutter_redux)
前面几个库,大体上来讲,只是提供了基本的数据管理能力,也就是,专心做好自己数据通道的职责,并未对具体实现做过多的限制的话。
相比之下,Redux就重得多了,它不只是做了个数据通道,还对整个数据层的操作进行了比较强的约束,相当于提供了比较完整的数据层设计模式。场景比较简单的话,这会显得代码很累赘;但项目比较大场景比较复杂的话,这些约束可以有效阻止代码的腐烂。
我们来看一下Redux的核心概念:
Redux有一个Store用于存储数据,我们的Model放在Store.state这个属性中,对数据的get操作,跟前面的框架并没有太大区别,当Store中数据变化时也会触发View的刷新;而对数据的set操作,Redux提出了很高的要求。
Provider框架下的Model默认其实是个比较重的model(当然你也可以进一步拆分),数据从model里出,view需要修改数据时调用model的方法,甚至可以直接修改model的属性然后抛个notifyListeners
出来。
这在Redux的模式下是不允许的。Redux的理由是:在这种不受约束的情况下,可能处处都会去改model,当业务越来越复杂的时候,这里状态的变化很可能会变得难以追溯:state在什么时候、出于什么原因、如何发生变化变得不受控制。Redux的所有设计的出发点就在这里:让state的变化变得可预测。
因此Redux对state的set进行了高度封装,为了所有state的修改有据可查,Redux引入了Action的概念,这个有点像客户端常用的Notification,每个通知有自己的标识。
具体使用上,Action可以是任意的类型,不需要携带数据的话可以用enum,给每种Action定义一个枚举;也可以给每个Action定义一个class,如:
class SearchLoadingAction {}
class SearchErrorAction {}
class SearchResultAction {
final SearchResult result;
SearchResultAction(this.result);
}
总之,Action应当能够区分类型,并且可以携带数据,通常只会带些比较简单的参数。
view层接收到用户输入时,产生一个Action通过Store进行分发store.dispatch(Actions.Increment)
因此,我们需要一个地方处理Action并更新state,这又引入了一个Reducer的概念。其实就是个处理Action更新State的函数。
当然它有一点特别的地方,你可以把State想成一个字典,每次Reducer改动State时,会对State做一次浅拷贝并修改其中的某个值,Store会存储新的State,而旧的State中的所有内容都是不变的,这称为不可变对象。很直接的好处是,这样修改的State,只要对比前后两个State的指针是否一致就知道State是否被修改过了(State中的属性可以嵌套很多层,道理是一样的,逐层比较指针即可)。此外,这种方式约束了数据的修改,提高了数据处理的安全性。并且,可以很方便地记录State变化的序列以及触发其变化的Action,方便调试、追溯数据变化的过程。
总结一下Redux的三大原则,其实上面已经提到了一部分了。
第一大原则是单一数据源。Redux建议整个应用的State被存储在一棵object tree中,并且这个object tree只存在于唯一一个store中。单Store和多Store一直以来有诸多争议,Redux认为集中式的Store更方便管理,也容易调试,并且避免了多个Store间同步数据的问题;而多Store则方便组件化、模块化的拆分,不同业务负责自己的Store也比较符合一般开发习惯。
第二大原则是state是只读的。前面已经讲得比较清楚了,为了让State的变化可预测,因此Redux中state是只读的,想要修改必现通过Action、Reducer进行修改。
第三大原则是用纯函数执行修改。也就是newState = oldState + Action这种方式,前面Reducer部分也讲得很清楚了。
fish-redux
刚刚,阿里宣布开源Flutter应用框架Fish Redux!
flutter_redux是基本完全遵循Redux规范的flutter实现。而闲鱼在实践过程中,在flutter-redux基础上进行了进一步的封装。
纯flutter应用使用flutter-redux是没什么问题的,但是很多应用是像闲鱼这样,在Native应用的基础上集成Flutter进行混合开发的,这种开发往往是以页面为单位的。因此对业务逻辑的分治、可插拔的组件化有比较强的需求,在这种背景下Redux应当如何实践,fish-redux给出了一个完整的框架。
看一下fish-redux的demo项目
fish-redux提出了Component的概念,可以理解为“Redux组件”,Component本身有完整的Redux能力(state/action/reducer),也可以方便地嵌入全局的Redux体系中。这套设计Fish-Redux自称可插拔的组件体系、既保留了Redux的原则又提供了业务代码分治的能力。
fish-redux涉及的新概念非常多,如果要用的话,对Redux应当有比较丰富的经验,并且,不建议简单应用使用这么重的框架。
BloC
基于流的响应式状态管理,由Google在2018的 DartConf首次提出。
毫无疑问,Stream或者更进一步的ReactiveX是纯粹的响应式编程,但其实Redux也通过Action、Reducer这样的方式实现了响应式(或许某种程度上不是特别纯粹,对异步的处理可能比起Stream的方式略有不足),因此实际使用时BloC和Redux其实会有一定的相似性。
BLoC代表业务逻辑组件(Business Logic Component),它的思想其实很朴素,基本上就是两点,第一是把业务逻辑抽离出来封装成独立的BLoC组件,这个其实每个数据流框架都是要这么做的;第二是使用流的方式构造响应式的数据流。
如上图所示,BLoC封装了所有的业务逻辑,通过Stream输出给Widget,而Widget产生的事件会抛给BLoC,BLoC处理后把数据塞进流的Sink中触发订阅者的更新。
BLoC本身只是个设计模式,并没有限定具体的实现,目前各方的实现也略有区别。可以通过InheritedWidget将BLoC注入组件树由子组件获取,也可以把BLoC做成一个单例去获取;流的部分可以用原生的Stream或RxDart。在此基础上还有一些更进一步的封装,能够减少一些重复代码。
这里我们看一下在flutter_bloc这个库基础上的BLoC实现:
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: BlocProvider(
create: (context) => CounterBloc(),
child: CounterPage(),
)
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder(
builder: (context, count) {
return Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => BlocProvider.of(context)
.add(CounterEvent.increment),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () => BlocProvider.of(context)
.add(CounterEvent.decrement),
),
)
],
),
);
}
}
enum CounterEvent { increment, decrement }
class CounterBloc extends Bloc {
@override
int get initialState => 0;
@override
Stream mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
yield state - 1;
break;
case CounterEvent.increment:
yield state + 1;
break;
}
}
}
子组件通过BlocProvider.of
拿到BLoC实例,通过Action的方式分发事件给BLoC,组件通过BlocBuilder
获取数据进行构建。
横向对比
框架选型
Flutter目前状态管理这块呈现出一个百花齐放的状态,简单总结一下。
仅使用state本身,在很小的应用上还能搞搞,中型应用是扛不住的,性能上缺乏有效的优化手段,数据层架构上也很难维护。
InheritedWidget本身用起来太麻烦,更多地是作为一种基础能力给上层框架,而不是直接用。
ScopedModel和Provider差不多是同类产品,能力上Provider更丰富一些并且有官方背书,一般的中小型应用都倾向于Provider一些。
Redux算是更复杂一点的数据层方案了,跟BLoC可以比较一下,这两者在响应式的路上比原始的State更进一步(或许BLoC更纯粹一些),Redux的优点是状态的变化比较可控,Action/Reducer的设计让状态的变化有理有据。BLoC的优点是对异步的处理更好一些。
最后说一下fish-redux,这其实是以Redux为核心定制的应用框架,并且很多理念是为混合应用而不是纯Flutter应用考虑的,虽然github上star很多...但是感觉实际上适合用它的应用并不多,中小型的用不上,大型的,还是混合应用,人家往往也有自己定制化的考量,或许更乐意从redux的基础上进行定制而不是拿fish-redux去改。
性能优化
横向对比大概就是这些,然后说说性能优化。
除了BLoC外,无论哪种方式将状态注入widget tree,想要获取状态,大体上都是两种方式
,一种是在build方法中使用Provider.of
获取Model,通常这会使得当前组件订阅Model,当Model发生变化时,引起当前组件rebuild;另一种是使用一个Consumer类的组件获取状态并传递给子组件,如:
Foo(
child: Consumer(
builder: (_, a, child) {
return Bar(a: a, child: child);
},
child: Baz(),
),
)
通常来讲,Comsumer类组件因为可以用builder方法来生成子组件,能做的优化会多很多。比如这个来自于Provider的例子,可以看到嵌套层级是Foo -> Bar -> Baz,使用Consumer封装后,其实只有Bar刷新了,上层和下层的Foo和Baz都没有rebuild,而如果使用`Provider.of
方式,子组件是必然会全部rebuild的。
此外,Provider还提供了一种Selector订阅的方式:
Selector(
selector: (_, list) => list.length,
builder: (_, length, __) {
return Text('$length');
}
);
此时只有selector的值产生变化时才会触发刷新,在这里也就是list.length。
flutter-redux也提供了类似方案:
StoreConnector(
distinct: true,
builder: (context, vm) {
return App(vm.isLogin, vm.key);
},
converter: (store) => AppViewModel.fromStore(store))
使用StoreConnector注入状态,不过要显式指定distinct: true才能依靠vm进行状态变化的过滤,一个使用Redux的中型应用,这应当是必须做的优化,毕竟,Redux秉承单一Store原则,牵一发而动全身。目前看起来框架作者甚至没有在readme里提这回事儿,感觉很多人用flutter-redux会有严重的性能问题。
Provider之流本身是推荐多个Model的,大部分widget只会依赖自己相关的model,潜在的性能风险要小很多,更何况Provider提供的性能优化能力还更加完善。
结语
综上,我推荐中小型应用使用Provider,功能丰富,使用简单,性能优化能力也提供得比较完整。