flutter ScopedModel深刻浅出

何为ScopedModel

ScopedModel是从Google正在开发的新系统Fuchsia库中分离出来,为了使用flutter时可以更好得管理flutter中的状态。ScopedModel是flutter最先开始使用的状态管理库。虽然目前它已经中止维护了,但仍是有不少人使用,而且,学习ScopedModel可以很轻松的学习flutter及了解flutter中状态管理的机制。git

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

开始的映射关系

当咱们项目复杂以后,咱们的程序将会有不少组件与视图与上百个状态,若是都经过子父之间传参那将会变得很是复杂redux

项目复杂以后

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

scoped_model可以提供将数据模型传递给它的全部后代以及在须要的时候从新渲染后代。bash

使用方法

使用方法比较简单,我用本身封装的Store作例子。app

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

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

引入依赖

...
dependencies:
  flutter:
    sdk: flutter
    
  scoped_model: ^1.0.1
...
复制代码

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

封装Store

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

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的理解欢迎讨论,若是我有错误的地方欢迎评论指出。

相关文章
相关标签/搜索