flutter中状态管理是重中之重,每当谈这个话题,总有说不完的话。

在正式介绍 ScopeModel 为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。
如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。
Flutter 状态管理之ScopeModel - 图1
但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。
Flutter 状态管理之ScopeModel - 图2
这又是什么鬼。我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
Flutter 实际上在一开始就为我们提供了一种状态管理方式,那就是 StatefulWidget。但是我们很快发现,它正是造成上述原因的罪魁祸首。
State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,我们增加非常多可怕的垃圾代码。
这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。

ScopeModel 是什么

ScopeModel是可让您轻松地将数据模型从父Widget传递到其后代。此外,更新模型时,它还会重建使用该模型的所有子代。该库最初是从Fuchsia代码库中提取的

首先在yaml中添加

  1. scoped_model: ^0.3.0

然后运行

  1. flutter pub get

获取到最新的包到本地,在需要的文件夹内导入

  1. import 'package:scoped_model/scoped_model.dart';

我们还用点击按钮新增数字的例子

新建Basemodel继承model

  1. class _BaseModel extends Model {
  2. int _count = 0;
  3. void add(int a) {
  4. _count += a;
  5. notifyListeners();
  6. }
  7. int get count => _count;
  8. }

然后在使用ScopedModel包裹小部件,一般用于包括一个页面或者需要更新的部件,交互和显示内容的需要在同一个ScopedModel下边。

  1. class BaseScopedPateRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return ScopedModel<_BaseModel>(
  5. model: _BaseModel(),
  6. child: _body(),
  7. );
  8. }
  9. static String get routeName => 'BaseScopedPate';
  10. Widget _body() => BaseScopedPate();
  11. }

然后是我们的UI

  1. class BaseScopedPate extends StatefulWidget {
  2. BaseScopedPate({Key key}) : super(key: key);
  3. @override
  4. _BaseScopedPateState createState() => _BaseScopedPateState();
  5. }
  6. class _BaseScopedPateState extends State<BaseScopedPate> {
  7. @override
  8. Widget build(BuildContext context) {
  9. return Scaffold(
  10. appBar: AppBar(
  11. title: Text('ScopedModel'),
  12. ),
  13. body: ScopedModelDescendant<_BaseModel>(
  14. builder: (context, child, model) {
  15. return Text('${model.count}');
  16. },
  17. ),
  18. floatingActionButton: FloatingActionButton(
  19. child: Icon(Icons.add),
  20. onPressed: () {
  21. ScopedModel.of<_BaseModel>(context).add(1);
  22. },
  23. ),
  24. );
  25. }
  26. }

最终效果是

Flutter 状态管理之ScopeModel - 图3

原理

我们BaseModel继承ScopedModel中的Model,而后者是继承了Listenable,在需要跟新的时候利用Microtask来回掉监听者,关键源码如下:

  1. /// 继承可以监听Listenable
  2. abstract class Model extends Listenable{
  3. /// 在下次事件循环中回掉所有监听者
  4. @protected
  5. void notifyListeners() {
  6. if (_microtaskVersion == _version) {
  7. _microtaskVersion++;
  8. scheduleMicrotask(() {
  9. _version++;
  10. _microtaskVersion = _version;
  11. _listeners.toList().forEach((VoidCallback listener) => listener());
  12. });
  13. }
  14. }
  15. }
  16. }

监听者添加时机

ScopedModel<BaseModel>(){}在执行builder的时候,调用了ModelBuilder小部件,该小部件实现了在initState(){}widget.model.addListen(_onChange)_onChange函数是void _onChange() => setState(() {});

至此监听者已经添加上去了,然后我们在继承Model中在需要刷新的时候则调用notifyListeners()则实现了刷新。 这样子刷新也和业务独立开来,这点和MVVM相似了。

局部刷新

当页面稍微复杂了,一个简单的点击事件导致整个页面刷新,这并不是我们想看到的,那么如何实现局部刷新呢?

我们定义多个modelA modelB分别继承model,然后在小部件顶部来监听,如下代码,然后在分别builder即可。

  1. class _BaseModel extends Model {
  2. int _count = 0;
  3. void add(int a) {
  4. _count += a;
  5. notifyListeners();
  6. }
  7. int get count => _count;
  8. }
  9. class _BaseModel2 extends Model {
  10. int _count = 0;
  11. void add(int a) {
  12. _count += a;
  13. notifyListeners();
  14. }
  15. int get count => _count;
  16. }

分别监听:

  1. class BaseScopedPateRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return ScopedModel<_BaseModel>(
  5. model: _BaseModel(),
  6. child: ScopedModel<_BaseModel2>(
  7. model: _BaseModel2(),
  8. child: _body(),
  9. ),
  10. );
  11. }
  12. static String get routeName => 'BaseScopedPate';
  13. Widget _body() => BaseScopedPate();
  14. }

在自己需要的地方去展示,这里只是展示关键代码,需要完整代码可以在文章底部查看链接。

  1. ScopedModelDescendant<_BaseModel>(
  2. builder: (context, child, model) {
  3. _build += '_BaseModel build';
  4. return Center(
  5. child: Text('${model.count}'),
  6. );
  7. },
  8. child: Text('外部小部件'),
  9. ),
  10. ScopedModelDescendant<_BaseModel2>(
  11. builder: (context, child, model) {
  12. _build += '_BaseModel2 build';
  13. return Center(
  14. child: Text('${model.count}'),
  15. );
  16. },
  17. child: Text('外部小部件'),
  18. ),

我们使用p1 build表示整个页面刷新一次,b1表示控件b刷新一个,b2表示b2刷新一次,出现几次就是刷新了几次。

看下最终效果:

Flutter 状态管理之ScopeModel - 图4

为什么p1总是出现在b1 b2之后呢?

留下一个问题给大家思考下,下期公布答案。

完整代码查看:github 查看