flutter的状态管理框架实践--ScopedModel

633 阅读4分钟

Scoped_Model使用步骤:

1.定义 Model 的实现,如 CountModel ,并且在状态改变时执行 notifyListeners() 方法。

import 'package:flutter/cupertino.dart';
import 'package:scoped_model/scoped_model.dart';
class CountModel extends Model {
  static CountModel of(BuildContext context) =>
      ScopedModel.of<CountModel>(context);

  int _count = 0;

  int get count => _count;

  void add() {
    _count++;
    notifyListeners();
  }
}

2.使用 ScopedModel 在父Widget 中加载 Model ,然后使用 ScopedModelDescendant 或者 ScopedModel.of(context) 加载 Model 内状态数据来实现子widget共享同一个CountModel状态。

以下就是两个子widget共享一个count的状态,子1widget点击按钮使count加一,子2widget显示的count也会同时变化加一,而父widget不会改变,。


class ScopedPage extends StatelessWidget {
  final CountModel _model = new CountModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("scoped"),
        ),
        body: Container(
          child: new ScopedModel<CountModel>(
            model: _model,
            //child: CountWidget(),
            child: Column(
              children: [
                //Text("父widget:"+_model.count.toString()),
//                ScopedModelDescendant<CountModel>(
//                    builder: (context,child,model){
//                      return Text("父widget:"+_model.count.toString());
//                    }
//                ),
                CountWidget(),
                CountWidget_2(),
              ],
            ),
          ),
        ));
  }
}
class CountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
          return new Column(
            children: <Widget>[
              new Container(child: new Center(child: new Text("子1widget:"+model.count.toString()))),
              new Row(
                children: [
                  new FlatButton(
                      onPressed: () {
                        model.add();
                      },
                      color: Colors.blue,
                      child: new Text("+")),
//                  new FlatButton(
//                      onPressed: () {
//                        Navigator.push(context, MaterialPageRoute(builder:(context)=>new SM_page4()));
//                      },
//                      color: Colors.blue,
//                      child: new Text("go")),
                ],
              ),
            ],
          );
        });
  }
}
class CountWidget_2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
          return new Column(
            children: <Widget>[
              new Container(child: new Center(child: new Text("子2widget:"+model.count.toString()))),
              new Row(
                children: [
                  new FlatButton(
                      onPressed: () {
                        model.add();
                      },
                      color: Colors.blue,
                      child: new Text("+")),
//                  new FlatButton(
//                      onPressed: () {
//                        Navigator.push(context, MaterialPageRoute(builder:(context)=>new SM_page4()));
//                      },
//                      color: Colors.blue,
//                      child: new Text("go")),
                ],
              ),
            ],
          );
        });
  }
}

效果图:

在实践中发现的几个点:

1.即使在父widget里面的小组件要共享model的状态,也要用ScopedModelDescendant包裹起来,不然不能共享model的状态

//还是原来的代码
class ScopedPage extends StatelessWidget {
  final CountModel _model = new CountModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("scoped"),
        ),
        body: Container(
          child: new ScopedModel<CountModel>(
            model: _model,
            //child: CountWidget(),
            child: Column(
              children: [
              //不加ScopedModelDescendant
                Text("不加ScopedModelDescendant父widget:"+_model.count.toString()),
              //加了ScopedModelDescendant
                ScopedModelDescendant<CountModel>(
                    builder: (context,child,model){
                      return Text("加了ScopedModelDescendant父widget:"+_model.count.toString());
                    }
                ),
                CountWidget(),
                CountWidget_2(),
              ],
            ),
          ),
        ));
  }
}

2.ScopedModel能不能跨页面共享model状态?

可以

但是如果想只是用Navigator来跳转到新页面,然后对新页面使用ScopedModelDescendant是会报错的,就像这样:

class CountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
          return new Column(
            children: <Widget>[
              new Container(child: new Center(child: new Text("子1widget:"+model.count.toString()))),
              new Row(
                children: [
                  new FlatButton(
                      onPressed: () {
                        model.add();
                      },
                      color: Colors.blue,
                      child: new Text("+")),
                  new FlatButton(
                      onPressed: () {
                      //就像这样
                        Navigator.push(context, MaterialPageRoute(builder:(context)=>new ScopedPage2()));
                      },
                      color: Colors.blue,
                      child: new Text("go")),
                ],
              ),
            ],
          );
        });
  }
}
class ScopedPage2 extends StatefulWidget{

  @override
  State<StatefulWidget> createState() {
    return new ScopedPage2State();
  }
}

class ScopedPage2State extends State<ScopedPage2> {

@override
  Widget build(BuildContext context) {
    // TODO: implement build
    return ScopedModelDescendant<CountModel>(
      builder: (context,child,model){
        return Scaffold(
          appBar:AppBar (title:Text("SM_3:")),
          body: Column(
            children: [
              Text("子widget3:"+model.count.toString()),
              FlatButton(
                  onPressed: () {
                    model.add();
                  },
                  color: Colors.blue,
                  child: new Text("+"))
            ],
          ),
        );
      }
    );
  }
}

会出现这样的报错:

探索真相

自己想了想是不是新页面的widget没有获取到上下文context,还用了ScopedModelDescendant包裹那个widget还是不行,又想了用context传参的方案,但是通过context还要一层层找到父控件的context的model,如果那么麻烦scopedmodel早就没人用了,所以都没解决这个问题。

以下是scopedmodel共享model的原理:

最后google了一番才找到了答案,别人跨页面也有这个问题,原文在这:github.com/brianegan/s…

而开发者是这样回答的:

大致意思就是说:model是不会自动提供给所有页面的,如果想实现多个页面共享同一个model,有三个办法:

1.用MaterialApp包裹组件,然后把model写在MaterialApp的上面

2.用CategoryEditPage包裹另一个页面

3.把这个model传给下一个页面

尝试第一种办法:在原来代码的基础上包裹一个MaterialApp

class ScopedPage extends StatelessWidget {
  final CountModel _model = new CountModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("scoped"),
        ),
        body: Container(
          child: new ScopedModel<CountModel>(
            model: _model,
            //child: CountWidget(),
            //MaterialApp在这里!!
            child: MaterialApp(
              home: Column(
                children: [
                  Text("不加ScopedModelDescendant父widget:"+_model.count.toString()),
                  ScopedModelDescendant<CountModel>(
                      builder: (context,child,model){
                        return Text("加了ScopedModelDescendant父widget:"+_model.count.toString());
                      }
                  ),
                  CountWidget(),
                  CountWidget_2(),
                ],
              ),
            )
          ),
        ));
  }
}

终于成功了:

另外两种方法就不展示了。

总结

  • ScopedModel是观察者模式的一个实现,原理就是把model放在父widget,model里的方法实现监听model数据的改变,如果发生改变,父widget就会通知子widget也改变。
  • ScopedModel是支持全局状态管理的,但是要在第一个页面用MaterialApp或者每次跳转新页面也得把model穿过去,还或者对新页面用CategoryEditPage,操作起来对各个页面都有局限性。所以ScopedModel更适合用在同一个页面多个组件共享同一个model状态的局部状态管理。
  • ScopedModel的局限性还体现在需要手动调用notifylistener(),如果model的状态复杂,得在处理事件逻辑的方法里反复调用notifylistener(),并且还要注意调用时机,并没有彻底体现数据源改变反映在UI上的自动性。所以scopedmodel更适合用在model数据状态不复杂的项目上。
  • scolpedmodel中countmodel是要继承Model的,如果以后要换成别的状态管理模式,countmodel的代码就得重新调整,这也是这个模式的一个局限性。