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的理解歡迎討論,若是我有錯誤的地方歡迎評論指出。

相關文章
相關標籤/搜索