flutter ScopedModel深入淺出

JSShou發表於2019-09-10

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

相關文章