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

JarvanMo發表於2020-06-02

背景

很久之前,在我們的QQ群裡有位朋友一直想讓我出個[Provider](https://github.com/rrousselGit/provider)教程,但是我一直沒有允諾。因為我覺得如果寫入門級的教程,已經有官方文件了,已經有人寫了,如果要深入一些呢,我又不會。但最近不太一樣了,因為要水文了。

狀態管理

說到Flutter,我們很難迴避狀態管理。對於React的開發者來說,狀態管理並不陌生;但對於我們這種純原生開發者來說,還是有些陌生的。 Flutter是宣告式的,這意味著Flutter是通過更新UI來反映當前app的狀態:

Flutter狀態管理:Provider4 入門教程(一)
簡單來說,在Flutter中,如果我們想更新我們的控制元件,最基本的方式應該是setState()了。如果說我們一個頁面裡的元件不多,直接使用setState()並沒有什麼問題,但是實際工作中,我們的頁面佈局還是足夠複雜的。

一種情況是我們在一個頁面中:

Flutter狀態管理:Provider4 入門教程(一)
如果我們把所有的Widget都寫到一個類裡,這個類一定會是個200多斤的胖子,而且很容易陷入{{{{}}}}旋渦。這時我們會想到Widget進行拆分,但這個時候如果僅僅依靠setState(),你會發現這將十分痛苦,因為setState()的作用域僅限於當作Widget,也就是說如果你僅僅在最底層的Widget裡呼叫setState並不會更新頂層的Widget,這就意味你要通過回撥實現,而且在這個過程中你會發現一些Widget類裡的變數又必須是不可變的,這又會引起其他的麻煩事,不談。

而通常來說,實際開發中,很可能有跨頁面共享資料的可能:

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

上圖為大家展示了一個物車功能,當使用者點選Add時,會將商品新增到購物車,點選購物車時,我們可以看到剛剛的商品。想想如果不使用狀態管理,我們應該如何實現呢?

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

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

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

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

初識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: ...
)
複製程式碼

如果想將隨時間變化而變化的變數傳遞到我們的物件中,可以考慮使用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的行為很像,這也是在上面擴充套件方法出現之前,我們獲取資料的方式。

這些方法會從控制元件樹中進行查詢,並且是從與傳遞過來的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),
        ),
      ),
    );
  }
}

複製程式碼

當我們高高興興的執行上面的程式碼時卻遇到了一些問題:

Flutter狀態管理:Provider4 入門教程(一)
當我第一次遇到這個錯誤的時候,我不由自主的說了一句以F開頭以U結尾的話。但是話說了也不能不解決問題,這個時候,我們可能需要Consumer

Consumer的使用

Consumer本身沒有魔法,也沒有什麼花裡胡哨的實現。只不過是在一個新的控制元件中使用Provider.of,然後將這個控制元件的build方法委託給引數裡的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。

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

相關文章