flutter ScopedModel深入浅出

878 阅读5分钟

何为ScopedModel

ScopedModel是从Google正在开发的新系统Fuchsia库中分离出来,为了使用flutter时能够更好得管理flutter中的状态。ScopedModel是flutter最早开始使用的状态管理库。虽然目前它已经停止维护了,但还是有很多人使用,并且,学习ScopedModel能够很轻松的学习flutter及了解flutter中状态管理的机制。

状态管理是什么,简单来说当我们项目构建起来,也许开始很简单,直接把一些组件映射成视图就行了,我用一个比较出名的图展示一下

开始的映射关系

当我们项目复杂之后,我们的程序将会有很多组件与视图与上百个状态,如果都通过子父之间传参那将会变得非常复杂

项目复杂之后

这时我们这些状态就很复杂了,我们维护起来可能会哭。那就需要状态管理了。

scoped_model能够提供将数据模型传递给它的所有后代以及在需要的时候重新渲染后代。

使用方法

使用方法比较简单,我用自己封装的Store做例子。

查看官方介绍中的使用方法pub.dev/packag...

查看官方例子代码github.com/brianega...

引入依赖

...
dependencies:
  flutter:
    sdk: flutter
    
  scoped_model: ^1.0.1
...

查看最新依赖包 pub.dev/packages/sc…

封装Store

Store类作为ScopedModel的入口与出口,所有有关ScopedModel的操作都通过此类,这样的好处是职责清晰,且后期维护更容易。

class MyStoreScoped {
  //  我们将会在main.dart中runAPP实例化init
  static init({context, child}) {
    return ScopedModel<Counter>(
      model: Counter(),
      child: ScopedModel<UserModel>(
        model: UserModel(),
        child: child,
      ),
    );
  }

  //  通过Provider.value<T>(context)获取状态数据
  static T value<T extends Model>(context) {
    return ScopedModel.of<T>(context, rebuildOnChange: true);
  }

  /// 通过Consumer获取状态数据
  static ScopedModelDescendant connect<T extends Model>({@required builder}) {
    return ScopedModelDescendant<T>(builder: builder);
  }
}

这里我引入了两个Model,Counter与UserModel,此例子中只使用了Counter,这样写只是提供一个思路,当我们有多个model需要引入的时候,我们可以把这个嵌套放到这里,如果实在过多,可以再写个递归方法来封装。

下面value方法和connect方法是用来获取及操作model实例,后面会讲。

创建Model

class Counter extends Model {
  int count = 0;

  void increment() {
    count++;
    notifyListeners();
  }

  void decrement() {
    count--;
    notifyListeners();
  }
}

顶层引入Model

//创建顶层状态
  @override
  Widget build(BuildContext context) {
    return MyStoreScoped.init(
        context: context,
        child: new MaterialApp(
        home: FirstPage(),
      ),
    );
  }

获取Model

获取和修改Model的值有两种方法,第一种是通过==ScopedModel.of(context, rebuildOnChange: true)==

...
//  通过Provider.value<T>(context)获取状态数据
  static T value<T extends Model>(context) {
    return ScopedModel.of<T>(context, rebuildOnChange: true);
  }
...

然后

Widget build(BuildContext context) {
    print('second page rebuild');
    Counter model = MyStoreScoped.value<Counter>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('SecondPage'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            RaisedButton(
              child: Text('+'),
              onPressed: () {
                model.increment();
              },
            ),
            Builder(
              builder: (context) {
                print('second page counter widget rebuild');
                return Text('second page: ${model.count}');
              },
            ),
            RaisedButton(
              child: Text('-'),
              onPressed: () {
                model.decrement();
              },
            ),
          ],
        ),
      ),
    );
  }

scoped_model原理

当点击+和-时,中间的数字将会变化。不过这种方式需要注意,当我们使用的时候,因为rebuildOnChange传的true,Model里面数据的任何变化都会引起整个build的重新渲染,而且如果存在在路由栈中的页面也通过此方式使用了Model,也会引起路由栈中的页面重新渲染。所以滥用此方式,在一定程度上肯定会引起页面性能的不好,第二种方式能够很好的解决这个问题。

第二种方式是使用==ScopedModelDescendant(builder: builder)==

  static ScopedModelDescendant connect<T extends Model>({@required builder}) {
    return ScopedModelDescendant<T>(builder: builder);
  }

使用

@override
  Widget build(BuildContext context) {
    print('first page rebuild');
    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            MyStoreScoped.connect<Counter>(builder: (context, child, snapshot) {
              return RaisedButton(
                child: Text('+'),
                onPressed: () {
                  snapshot.increment();
                },
              );
            }),
            MyStoreScoped.connect<Counter>(builder: (context, child, snapshot) {
              print('first page counter widget rebuild');
              return Text('${snapshot.count}');
            }),
            MyStoreScoped.connect<Counter>(builder: (context, child, snapshot) {
              return RaisedButton(
                child: Text('-'),
                onPressed: () {
                  snapshot.decrement();
                },
              );
            }),
            MyStoreScoped.connect<UserModel>(
                builder: (context, child, snapshot) {
              print('first page name Widget rebuild');
              return Text('${MyStoreScoped.value<UserModel>(context).name}');
            }),
            TextField(
              controller: controller,
            ),
            MyStoreScoped.connect<UserModel>(
                builder: (context, child, snapshot) {
              return RaisedButton(
                child: Text('change name'),
                onPressed: () {
                  snapshot.setName(controller.text);
                },
              );
            }),
          ],
        ),
      )
    );
  }

这种方式通过ScopedModelDescendant包裹起来,通过builder返回的第三个参数使用model。实际上这种方式实现的原理也还是使用的==ScopedModel.of(context, rebuildOnChange: true)==,不过里面使用了一个Widget,通过这个Widget的build方法返回的context把需要重新渲染的区域限制在了builder返回的Widget下,对于复杂的页面及对性能有很高要求的页面,此方式会大大提高程序的性能。

此例子代码传到我github的日常demo中,具体代码查看github.com/xuzhongpeng…

实现原理

一图胜千言

scoped_model原理

ScopedModel有四个重要的部分,Model,ScopedModel,AnimatedBuilder,InheritedWidget

model

Model类继承继承Listenable,它主要会提供一个notifyListeners()方法

ScopedModel及AnimatedBuilder

当我们使用ScopedModel在顶层注册Model的时候,ScopedModel内部使用了一个AnimatedBuilder的类,它会把model的实例传入此类的第一个参数,当model中调用notifyListeners()时,会重新渲染此类下的子组件。

...
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: model,
      builder: (context, _) => _InheritedModel<T>(model: model, child: child),
    );
  }
...
class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
}

AnimatedBuilder继承自AnimatedWidget,其中会调用addListener()方法添加一个监听者,Model继承Listenable类,当我们调用notifyListeners()时会使AnimatedBuilder中的_handleChange()执行,然后调用setState()方法进行rebuild。这也是为什么在修改值后需要调用notifyListeners()的原因。

InheritedWidget

AnimatedBuilder第二个参数返回一个_InheritedModel是继承自InheritedWidget的类,InheritedWidget类可以很方便得让所有子组件中方便的查找祖父元素中的model实例。

class _InheritedModel<T extends Model> extends InheritedWidget {     
  final T model;                                                     
  final int version;                                                 
                                                                     
  _InheritedModel({Key key, Widget child, T model})                  
      : this.model = model,                                          
        this.version = model._version,                               
        super(key: key, child: child);                               
                                                                     
  @override                                                          
  bool updateShouldNotify(_InheritedModel<T> oldWidget) =>           
      (oldWidget.version != version);                                
}

InheritedWidget可以在组件树中有效的传递和共享数据。将InheritedWidget作为 root widget,child widget可以通过inheritFromWidgetOfExactType()方法返回距离它最近的InheritedWidget实例,同时也将它注册到InheritedWidget中,当InheritedWidget的数据发生变化时,child widget也会随之rebuild。

当InheritedWidget rebuild时,会调用updateShouldNotify()方法来决定是否重建 child widget。

当我们调用Model的notifyListeners()方法时,version就会自增,然后InheritedWidget使用version来判断是否需要通知child widget更新。

需要注意一个地方,AnimatedBuilder这个添加监听后如果执行notifyListeners()会重新渲染其builder返回的值,但是如果我们够细心会发现其子组件是没有重新渲染的(以MaterialApp为例),这是因为MaterialApp是作为一个参数传递给ScopedModel,而ScopedModel中使用了一个child变量将其缓存了起来,所以在执行setState的时候,并不会重新渲染MaterialApp。

总结

ScopedModel是利用了AnimatedBuilder与InheritedWidget去实现了其状态管理机制,其实像provider,redux都是通过类似的方式去实现的,稍微有点变化的可能就是订阅通知机制的使用,比如redux是使用的Stream实现的。以上是我对ScopedModel的理解欢迎讨论,如果我有错误的地方欢迎评论指出。