從零開始的Flutter之旅: Provider

往期回顧

從零開始的Flutter之旅: StatelessWidgetgit

從零開始的Flutter之旅: StatefulWidgetgithub

從零開始的Flutter之旅: InheritedWidget緩存

在上篇文章中咱們介紹了InheritedWidget,並在最後引起出一個問題。bash

雖然InheritedWidget能夠提供共享數據,而且經過getElementForInheritedWidgetOfExactType來解除didChangeDependencies的調用,但仍是沒有避免CountWidget的從新build,並無將build最小化。微信

咱們今天就來解決如何避免沒必要要的build構建,將build縮小到最小的CountText。網絡

分析

首先咱們來分析下爲何會致使父widget的從新build。架構

class CountWidget extends StatefulWidget {
  @override
  _CountState createState() {
    return _CountState();
  }
}
 
class _CountState extends State<CountWidget> {
  int count = 0;
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Count App',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Count"),
        ),
        body: Center(
          child: CountInheritedWidget(
            count: count,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                CountText(),
                RaisedButton(
                  onPressed: () => setState(() => count++),
                  child: Text("Increment"),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}
複製代碼

爲了方便分析,我把以前的代碼提到這裏來。app

咱們來看,在點擊RaisedButton的時候,咱們會經過setState將count進行更新。而此時的setState方法的提供者是_CountState,即CountWidget。而state的改變會致使build的從新構建,致使的效果是CountWidget的build被從新調用,繼而它的子widget也相繼被從新build。框架

既然已經知道了緣由,那麼咱們再來思考下解決方案。less

  1. 最簡單的,咱們縮小setState提供者的範圍。如今是CountWidget,咱們將其縮小到Column。
  2. 雖然已經縮小到了Column,但仍是沒法避免自身的build與其CountText以外的子Widget(RaisedButton)的從新build。若是咱們將Column所有緩存下來呢?咱們在Column外層套一個Widget,並將其進行緩存,一旦外層的Widget從新build,咱們都使用Column的緩存,這樣不就避免了Column的從新build。不過使用緩存之後會有個問題,既然是緩存,Center裏面的CountText也將不會改變。爲了解決這個問題,咱們就要使用上篇文章中的InheritedWidget。將整個Column放到InheritedWidget中,雖然Column是緩存,可是CountText中引用了InheritedWidget中的count數據,一旦count發生改變,將會通知其進行從新build。這樣就保證了只刷新CountText。

若是你對InheritedWidget不熟悉,推薦閱讀從零開始的Flutter之旅: InheritedWidget

咱們來總結一下,在Column外套一層Widget,並將Column進行緩存,而後外層的Widget結合InheritedWidget來提供共享count的數據源。一旦count更新將會調用外層Widget的setState,而且從新build,但咱們使用的是Column緩存,同時CountText經過依賴的方式引用了共享的count數據源,從而會同步build更新。而RaisedButton使用的是未依賴的共享count數據源,因此並不會從新build。這樣就保證了只刷新CountText。

這種方式統必定義爲Provider,其實Flutter內部已經有Provider的完整實現,不過咱們爲了學習這種解決方法的思想,本身來實現一個簡易版的Provider。以後再去看Flutter的Provider將會更加簡單。

方案已經有了,下面咱們直接來看具體實現細節。

實現

  1. 定義共享數據的ProviderInheritedWidget
  2. 定義監聽刷新的NotifyModel
  3. 提供緩存Widget的ModelProviderWidget
  4. 組裝替換原有實現方案

ProviderInheritedWidget

實現一個本身的InheritedWidget,主要用來提供共享數據源,並接受緩存的child。

class ProviderInheritedWidget<T> extends InheritedWidget {
  final T data;
  final Widget child;
 
  ProviderInheritedWidget({@required this.data, this.child})
      : super(child: child);
 
  @override
  bool updateShouldNotify(ProviderInheritedWidget oldWidget) {
    // true -> 通知樹中依賴改共享數據的子widget
    return true;
  }
}
複製代碼

NotifyModel

爲了監聽共享數據count的變化,咱們經過觀察者訂閱模式來實現。

class NotifyModel implements Listenable {
  List _listeners = [];
 
  @override
  void addListener(listener) {
    _listeners.add(listener);
  }
 
  @override
  void removeListener(listener) {
    _listeners.remove(listener);
  }
 
  void notifyDataSetChanged() {
    _listeners.forEach((item) => item());
  }
}
複製代碼

Listenable提供一個簡單的監聽接口,經過add與remove來增長與移除監聽,而後提供一個notify方法來進行通知監聽者。

最後咱們經過繼承NotifyModel來使count具備可監聽能力

class CountModel extends NotifyModel {
  int count = 0;
 
  CountModel({this.count});
 
  void increment() {
    count++;
    notifyDataSetChanged();
  }
}
複製代碼

一旦count自增,就調用notifyDataSetChanged來通知訂閱的監聽者。

ModelProviderWidget

有了上面的Provider與Model,咱們在提供一個外部Widget來統一管理它們,將它們結合起來。

class ModelProviderWidget<T extends NotifyModel> extends StatefulWidget {
  final T data;
 
  final Widget child;
 
  // context 必須爲當前widget的context
  static T of<T>(BuildContext context, {bool listen = true}) {
    return (listen ? context.dependOnInheritedWidgetOfExactType<ProviderInheritedWidget<T>>()
            : (context.getElementForInheritedWidgetOfExactType<ProviderInheritedWidget<T>>()
        .widget as ProviderInheritedWidget<T>)).data;
  }
 
  ModelProviderWidget({Key key, @required this.data, @required this.child})
      : super(key: key);
 
  @override
  _ModelProviderState<T> createState() => _ModelProviderState<T>();
}
 
class _ModelProviderState<T extends NotifyModel>
    extends State<ModelProviderWidget> {
  void notify() {
    setState(() {
      print("notify");
    });
  }
 
  @override
  void initState() {
    // 添加監聽
    widget.data.addListener(notify);
    super.initState();
  }
 
  @override
  void dispose() {
    // 移除監聽
    widget.data.removeListener(notify);
    super.dispose();
  }
 
  @override
  void didUpdateWidget(ModelProviderWidget<T> oldWidget) {
    // data 更新時移除老的data監聽
    if (oldWidget.data != widget.data) {
      oldWidget.data.removeListener(notify);
      widget.data.addListener(notify);
    }
    super.didUpdateWidget(oldWidget);
  }
 
  @override
  Widget build(BuildContext context) {
    return ProviderInheritedWidget<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}
複製代碼

在這裏咱們提供可監聽的data數據與須要緩存的child,同時在state中對可監聽的data在合適的地方進行監聽訂閱與移除訂閱,並在收到data數據改變時調用notify進行setState操做,通知widget刷新。

在build中引用了ProviderInheritedWidget,來實現對共享子widget的數據共享,同時在ModelProviderWidget中提供of方法來暴露ProviderInheritedWidget的統一獲取方式。

經過參數listen(默認true)來控制獲取共享數據的方式,來決定是否創建依賴關係,即共享數據改變時,引用共享數據的widget是否從新build。

這一幕是否是有點似曾相識,基本上都是上篇文章中提到的InheritedWidget使用的細節。

接下來就是最終的方案替換

組裝替換原有實現方案

咱們經過ModelProviderWidget.of來獲取共享的數據,因此只要使用到了共享數據,將要調用該方法。爲了不沒必要要的重複書寫,咱們將其單獨封裝到Consumer中,內部來實現對其的調用,而且將調用的結果暴露出來。

class Consumer<T> extends StatelessWidget {
  final Widget Function(BuildContext context, T value) builder;
 
  const Consumer({Key key, @required this.builder}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    print("Consumer build");
    return builder(context, ModelProviderWidget.of<T>(context));
  }
}
複製代碼

一切準備就緒,咱們再對以前的代碼進行優化。

class CountWidget extends StatefulWidget {
  @override
  _CountState createState() {
    return _CountState();
  }
}
 
class _CountState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print("CountWidget build");
    return MaterialApp(
      title: 'Count App',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Count"),
        ),
        body: Center(
          child: ModelProviderWidget<CountModel>(
            data: CountModel(count: 0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Consumer<CountModel>(
                    builder: (context, value) => Text("count: ${value.count}")),
                Builder(
                  builder: (context) {
                    print("RaiseButton build");
                    return RaisedButton(
                      onPressed: () => ModelProviderWidget.of<CountModel>(
                              context,
                              listen: false)
                          .increment(),
                      child: Text("Increment"),
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
複製代碼

咱們將Column緩存到ModelProviderWidget中,同時對CountModel數據進行共享;經過Consumer進行Text的封裝,引用共享數據CountModel中的count。

對於RaisedButton,由於它只是提供點擊,而且觸發count的自增操做、沒有發生ui上的任何變化。因此爲了不RaisedButton引用的共享數據進行自增時從新build,這裏將listen參數置爲false。

最後咱們運行上面的代碼,咱們點擊Increment按鈕時,控制檯將會輸出以下日誌:

I/flutter ( 3141): notify
I/flutter ( 3141): Consumer build
複製代碼

說明只有Consumer從新調用了build,即Text進行了刷新。其它的widget都沒有變化。

這樣就解決了開篇提到的疑問,達到了widget刷新的最小化。

以上是一個簡單地Provider-Consumer的使用。Flutter對這一塊有更完善的實現方案。可是通過咱們這一輪分析,你再去看Flutter中Provider的源碼將會更加簡單易懂。

若是你想了解Flutter中Provider的使用,你能夠經過flutter_github來了解它的具體實戰使用技巧。

想要查看Provider實戰技巧,須要將分支切換到sample_provider

推薦項目

下面介紹一個完整的Flutter項目,對於新手來講是個不錯的入門。

flutter_github,這是一個基於Flutter的Github客戶端同時支持Android與IOS,支持帳戶密碼與認證登錄。使用dart語言進行開發,項目架構是基於Model/State/ViewModel的MSVM;使用Navigator進行頁面的跳轉;網絡框架使用了dio。項目正在持續更新中,感興趣的能夠關注一下。

固然若是你想了解Android原生,相信flutter_github的純Android版本AwesomeGithub是一個不錯的選擇。

若是你喜歡個人文章模式,或者對我接下來的文章感興趣,建議您關注個人微信公衆號:【Android補給站】

或者掃描下方二維碼,與我創建有效的溝通,同時更快更準的收到個人更新推送。

相關文章
相關標籤/搜索