Flutter狀態管理:Provider4 入門教程(一)

背景

好久以前,在咱們的QQ羣裏有位朋友一直想讓我出個[Provider](https://github.com/rrousselGit/provider)教程,可是我一直沒有允諾。由於我以爲若是寫入門級的教程,已經有官方文檔了,已經有人寫了,若是要深刻一些呢,我又不會。但最近不太同樣了,由於要水文了。html

狀態管理

說到Flutter,咱們很難迴避狀態管理。對於React的開發者來講,狀態管理並不陌生;但對於咱們這種純原生開發者來講,仍是有些陌生的。 Flutter是聲明式的,這意味着Flutter是經過更新UI來反映當前app的狀態:git

簡單來講,在 Flutter中,若是咱們想更新咱們的控件,最基本的方式應該是 setState()了。若是說咱們一個頁面裏的組件很少,直接使用 setState()並無什麼問題,可是實際工做中,咱們的頁面佈局仍是足夠複雜的。

一種狀況是咱們在一個頁面中: github

若是咱們把全部的 Widget都寫到一個類裏,這個類必定會是個200多斤的胖子,並且很容易陷入 {{{{}}}}旋渦。這時咱們會想到 Widget進行拆分,但這個時候若是僅僅依靠 setState(),你會發現這將十分痛苦,由於 setState()的做用域僅限於看成 Widget,也就是說若是你僅僅在最底層的 Widget裏調用 setState並不會更新頂層的 Widget,這就意味你要經過回調實現,並且在這個過程當中你會發現一些 Widget類裏的變量又必須是不可變的,這又會引發其餘的麻煩事,不談。

而一般來講,實際開發中,極可能有跨頁面共享數據的可能: 編程

上圖爲你們展現了一個物車功能,當用戶點擊Add時,會將商品添加到購物車,點擊購物車時,咱們能夠看到剛剛的商品。想一想若是不使用狀態管理,咱們應該如何實現呢?api

說了這麼多,無非就是想說使用狀態管理的更要性。簡單來講就是如何方便快捷地在Widget之間共享數據並將數據展現在頁面上。app

Flutter的狀態管理方式包括但不限於ProviderBlocRedux以及Fish-Reduxless

  • Bloc準確地來講是一種理念,也是我使用的第一個狀態管理,如今也有對應的實現庫,通常來講是基於響應式編程的。
  • Redux對於React開發者來講並不陌生,畢竟Flutter這塊也是借鑑了React
  • Fish-Redux脫胎於Redux,阿里出品,整體來講比較複雜,適合中大型項目。如今社會也有生成Fish-Redux模板代碼的工具
  • ProviderGoogle推薦的狀態管理,也是我使用的第二種狀態管理,相對來講比較簡單省心。

接下來,我將簡單地介紹一下Provider的使用。ide

初識Provider

Provider實際上是對InheritedWidget的封裝。相比於直接使用InheritedWidget,使用Provider有不少好處,好比說簡化資源的分配與處置,支持懶加載等等。工具

Provider 爲咱們提供了一些不一樣類型的Provider。要查看全部類型的provider能夠點擊這裏佈局

name description
Provider 最基礎的provider。它攜帶一個值並將這個值暴露,不管這個值是什麼。
ListenableProvider Listenable對象而建立的providerListenableProvider會監聽對象的變化,只要ListenableProvider的listner被調用,ListenableProvider就會從新構建依賴於該provider的控件。
ChangeNotifierProvider ChangeNotifierProvider是一種特殊的ListenableProvider,它基於ChangeNotifier,而且在有須要的時候,它會自動調用ChangeNotifier.dispose
ValueListenableProvider 監聽ValueListenable並只會暴露ValueListenable.value.
StreamProvider 監聽一個Stream 而且對外暴露最新提交的值。
FutureProvider 攜帶一個 Future,當Future完成時,它會更新依賴於它的控件。

鑑於本人才疏學淺,本文並不會逐一講述如何使用各類Provider,因此本文挑選了我用的最多的ChangeNotifierProvider來說解,但願能夠拋磚引玉。

建立一個Proivder

通常來講建立Provider有兩種方式:

  • 默認構造方法
  • .value構造方法

當咱們要新建立一個對象,咱們要使用默認構造方法而不是使用.value構造方法,由於若是咱們經過.value建立一個對象可能會引發內存泄漏或產生一些意想不到的問題。 這裏簡單解釋一下爲何不能使用value建立一個對象,英文好的能夠看StackOverflow原文。由於Flutter中的build方法應該是純淨無反作用的,不少外部因素會觸發rebuild,好比說:

  • 路由的pop/push
  • 屏幕大小從新調整,一般來講是由於鍵盤變化或者屏幕方向變化
  • 父控件重繪子控件
  • 依賴於InheritedWidget的控件(Class.of(context) 部分)發生了變化

因此說,使用.value建立對象的問題在於會使得build變得不純粹或者說具備反作用,會使來自外部的build調用變得很麻煩。 這個問題到此爲止,喜歡研究的朋友能夠自行探索。

  • 使用Providercreate中建立對象。
Provider(
  create: (_) => MyModel(),
  child: ...
)
複製代碼
  • 不要 使用Provider.value建立對象。
ChangeNotifierProvider.value(
 value: MyModel(),
 child: ...
)
複製代碼
  • 不要 從能夠隨時間變化而變化的變量中建立對象。 由於在這種狀況中,即便引用的變量發生了變化,咱們建立的對象也不會被更新。
int count;

Provider(
  create: (_) => MyModel(count),
  child: ...
)
複製代碼

If you want to pass variables that can change over time to your object, consider using ProxyProvider: 若是想將隨時間變化而變化的變量傳遞到咱們的對象中,能夠考慮使用ProxyProvider

int count;

ProxyProvider0(
  update: (_, __) => MyModel(count),
  child: ...
)
複製代碼

筆記:當使用Providercreate/update回調時,咱們要注意的是,默認狀況下,create/update的調用是懶式調用的。這就意味着,只有咱們Provider中的數據至少被請求一次,create/update纔會被調用。若是咱們想作一些預處理,咱們可使用lazy參數來禁止這一特性:

MyProvider(
  create: (_) => Something(),
  lazy: false,
)
複製代碼

讀取Provider中的數據

最簡單的讀取數據的方式是使用BuildContext的擴展方法:

  • context.watch(), 該方法會使用對應的控件監聽T的變化。
  • context.read(), 該方法直接返回T,並不會監聽的變化。
  • context.select<T, R>(R cb(T value)), 該方法會使對應的控件只監聽一小部分T的變,從名字上看咱們就知道這是一個篩選器。

固然了咱們也可使用靜態方法Provider.of<T>(context),它和watch/read的行爲很像,這也是在上面擴展方法出現以前,咱們獲取數據的方式。

These methods will look up in the widget tree starting from the widget associated with the BuildContext passed, and will return the nearest variable of type T found (or throw if nothing is found).

It's worth noting that this operation is O(1). It doesn't involve actually walking in the widget tree. 這些方法會從控件樹中進行查找,而且是從與傳遞過來的BuildContext相關的控件開始,最終返會找到並返回與類型T的最近變量(若是未找到,則拋出)。

值得注意的是,這個操做的複雜度爲O(1)。 實際上,這並不會在控件樹中游走。

說到如今,無非仍是對文檔的翻譯,如今讓咱們走碼上任吧~~~

Show me the code

故事仍是要從Flutter的計數器提及,由於新建立的Flutter項目模板就是這個計數器了,如今咱們要用ChangeNotifierProvider來簡單改造一下這個項目。

  • MyHomePageStatefulWidget改爲StatelessWidget
  • 使用ChangeNotifierProvider來更新頁面

第一個版本

首先,咱們要建立一個ChangeNotifier

class MyChangeNotifier extends ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  incrementCounter() {
    _counter++;
    notifyListeners();//要更新UI記得調用這個方法
  }
}
複製代碼

當前咱們點擊FloatingActionButton時會調用MyChangeNotifierincrementCounter方法,要注意的是當咱們處理完業務時,若是須要更新UI須要調用notifyListeners來通知Provider更新UI。

接下來咱們實現咱們的UI。

首先,咱們要建立MyHomePage, UI佈局直接使用的是example裏的佈局,不一樣的是咱們使用的是StatelessWidget。而後咱們經過BuildContext取出MyChangeNotifier實例。要注意到,當咱們點擊FloatingActionButton,咱們並無調用setState(廢話,StatelessWidget也不能setState),但咱們的UI依然會被更新。代碼以下:

class MyHomePage extends StatelessWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    MyChangeNotifier notifier =
        Provider.of(context); //經過Provider.of(context)獲取MyChangeNotifier
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${context.watch<MyChangeNotifier>().counter}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: notifier.incrementCounter,//點擊時咱們指望輸出點擊次數
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

複製代碼

如今咱們要用ChangeNotifierProvider包裹MyHomePage,這樣能夠保證在MyHomePage中能夠經過BuildContext取到MyChangeNotifier實例。

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData(
       primarySwatch: Colors.blue,
       visualDensity: VisualDensity.adaptivePlatformDensity,
     ),
     home: ChangeNotifierProvider(
         create: (_) => MyChangeNotifier(),
         child: MyHomePage(title: 'Flutter Demo Home Page')),
   );
 }
}
複製代碼

到此爲止,代碼已經寫完了,運行下,效果是否是和example如出一轍呢?

固然了,咱們能夠直接在MyChangeNotifier中直接定義一個字段叫outputMessage,而後直接在MyHomePage中直接給Text賦值。

Text(
    context.watch<MyChangeNotifier>().outputMessage,
    style: Theme.of(context).textTheme.headline4,
    ),
複製代碼

第二個版本

如今看來,咱們已經學會了ChangeNotifierProvider的基本用法,那麼咱們如今要對上面的代碼簡單改造一下。

  • ChangeNotifierProvider移動到MyHomePage

很簡單了,代碼以下:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;
  final MyChangeNotifier notifier = MyChangeNotifier();

  MyHomePage({Key key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => notifier,
      child: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${context.watch<MyChangeNotifier>().counter}',
                style: Theme.of(context).textTheme.headline4,
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: notifier.incrementCounter,//點擊時咱們指望輸出點擊次數
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

複製代碼

當咱們高高興興的運行上面的代碼時卻遇到了一些問題: Simulator Screen Shot - iPhone 11 Pro Max - 2020-06-02 at 19.18.24.png

當我第一次遇到這個錯誤的時候,我不禁自主的說了一句以F開頭以U結尾的話。可是話說了也不能不解決問題,這個時候,咱們可能須要 Consumer

Consumer的使用

Consumer自己沒有魔法,也沒有什麼花裏胡哨的實現。只不過是在一個新的控件中使用Provider.of,而後將這個控件的build方法委託給lamda裏的builder。這個builder會被調用屢次。就是這麼簡單。

Consumer的設計初衷有兩個

  • 當咱們的BuildContext中不存在指定的Provider時,Consumer容許咱們從Provider中的獲取數據。
  • 經過提供更多細小的重繪達到性能的優化。

咱們如今遇到的就是第一種狀況,至於第二種狀況,讀者們可自行探討。 因此,咱們能夠經過加一個Consumer來解決上面的ProviderNotFoundException問題:

class MyHomePage extends StatelessWidget {
  final String title;
  final MyChangeNotifier notifier = MyChangeNotifier();

  MyHomePage({Key key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => notifier,
      child: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Consumer<MyChangeNotifier>(
          builder: (_, localNotifier, __) => Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '${localNotifier.counter}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: notifier.incrementCounter,
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}
複製代碼

再次運行,是否是很完美?

暫時性總結

時間有限,原本想一口氣寫完,可是互聯網時代不玩玩飢餓營銷怎麼好意思說本身混過互聯網。。。

做爲Provider入門第一篇,本文仍是十分簡單的,畢竟只是改下了一下Flutter example。在接下來的文章中,我會介紹更多的Provider用法與問題,也包含更復雜的demo。

未完待續。。。 期待不期待你說了算。

相關文章
相關標籤/搜索