在上一篇文章中,我與你分享了如何在原生混編 Flutter 工程中管理混合導航棧,應對跨渲染引擎的頁面跳轉,即解決原生頁面如何切換到 Flutter 頁面,以及 Flutter 頁面如何切換到原生頁面的問題。
如果說跨渲染引擎頁面切換的關鍵在于,如何確保頁面跳轉的渲染體驗一致性,那么跨組件(頁面)之間保持數據共享的關鍵就在于,如何清晰地維護組件共用的數據狀態了。在第 20 篇文章“[關于跨組件傳遞數據,你只需要記住這三招](https://time.geekbang.org/column/article/116382)”中,我已經與你介紹了 InheritedWidget、Notification 和 EventBus 這 3 種數據傳遞機制,通過它們可以實現組件間的單向數據傳遞。
如果我們的應用足夠簡單,數據流動的方向和順序是清晰的,我們只需要將數據映射成視圖就可以了。作為聲明式的框架,Flutter 可以自動處理數據到渲染的全過程,通常并不需要狀態管理。
但,隨著產品需求迭代節奏加快,項目逐漸變得龐大時,我們往往就需要管理不同組件、不同頁面之間共享的數據關系。當需要共享的數據關系達到幾十上百個的時候,我們就很難保持清晰的數據流動方向和順序了,導致應用內各種數據傳遞嵌套和回調滿天飛。在這個時候,我們迫切需要一個解決方案,來幫助我們理清楚這些共享數據的關系,于是狀態管理框架便應運而生。
Flutter 在設計聲明式 UI 上借鑒了不少 React 的設計思想,因此涌現了諸如 flutter\_redux、flutter\_mobx 、fish\_redux 等基于前端設計理念的狀態管理框架。但這些框架大都比較復雜,且需要對框架設計概念有一定理解,學習門檻相對較高。
而源自 Flutter 官方的狀態管理框架 Provider 則相對簡單得多,不僅容易理解,而且框架的入侵性小,還可以方便地組合和控制 UI 刷新粒度。因此,在 Google I/O 2019 大會一經面世,Provider 就成為了官方推薦的狀態管理方式之一。
那么今天,我們就來聊聊 Provider 到底怎么用吧。
## Provider
從名字就可以看出,Provider 是一個用來提供數據的框架。它是 InheritedWidget 的語法糖,提供了依賴注入的功能,允許在 Widget 樹中更加靈活地處理和傳遞數據。
那么,什么是依賴注入呢?通俗地說,依賴注入是一種可以讓我們在需要時提取到所需資源的機制,即:預先將某種“資源”放到程序中某個我們都可以訪問的位置,當需要使用這種“資源”時,直接去這個位置拿即可,而無需關心“資源”是誰放進去的。
所以,為了使用 Provider,我們需要解決以下 3 個問題:
* 資源(即數據狀態)如何封裝?
* 資源放在哪兒,才都能訪問得到?
* 具體使用時,如何取出資源?
接下來,我通過一個例子來與你演示如何使用 Provider。
在下面的示例中,我們有兩個獨立的頁面 FirstPage 和 SecondPage,它們會共享計數器的狀態:其中 FirstPage 負責讀,SecondPage 負責讀和寫。
在使用 Provider 之前,我們**首先需要在 pubspec.yaml 文件中添加 Provider 的依賴**:
~~~
dependencies:
flutter:
sdk: flutter
provider: 3.0.0+1 #provider 依賴
~~~
添加好 Provider 的依賴后,我們就可以進行數據狀態的封裝了。這里,我們只有一個狀態需要共享,即 count。由于第二個頁面還需要修改狀態,因此我們還需要在數據狀態的封裝上包含更改數據的方法:
~~~
// 定義需要共享的數據模型,通過混入 ChangeNotifier 管理聽眾
class CounterModel with ChangeNotifier {
int _count = 0;
// 讀方法
int get counter => _count;
// 寫方法
void increment() {
_count++;
notifyListeners();// 通知聽眾刷新
}
}
~~~
可以看到,我們在資源封裝類中使用 mixin 混入了 ChangeNotifier。這個類能夠幫助我們管理所有依賴資源封裝類的聽眾。當資源封裝類調用 notifyListeners 時,它會通知所有聽眾進行刷新。
**資源已經封裝完畢,接下來我們就需要考慮把它放到哪兒了。**
因為 Provider 實際上是 InheritedWidget 的語法糖,所以通過 Provider 傳遞的數據從數據流動方向來看,是由父到子(或者反過來)。這時我們就明白了,原來需要把資源放到 FirstPage 和 SecondPage 的父 Widget,也就是應用程序的實例 MyApp 中(當然,把資源放到更高的層級也是可以的,比如放到 main 函數中):
~~~
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 通過 Provider 組件封裝數據資源
return ChangeNotifierProvider.value(
value: CounterModel(),// 需要共享的數據資源
child: MaterialApp(
home: FirstPage(),
)
);
}
}
~~~
可以看到,既然 Provider 是 InheritedWidget 的語法糖,因此它也是一個 Widget。所以,我們直接在 MaterialApp 的外層使用 Provider 進行包裝,就可以把數據資源依賴注入到應用中。
這里需要注意的是,由于封裝的數據資源不僅需要為子 Widget 提供讀的能力,還要提供寫的能力,因此我們需要使用 Provider 的升級版 ChangeNotifierProvider。而如果只需要為子 Widget 提供讀能力,直接使用 Provider 即可。
**最后,在注入數據資源完成之后,我們就可以在 FirstPage 和 SecondPage 這兩個子 Widget 完成數據的讀寫操作了。**
關于讀數據,與 InheritedWidget 一樣,我們可以通過 Provider.of 方法來獲取資源數據。而如果我們想寫數據,則需要通過獲取到的資源數據,調用其暴露的更新數據方法(本例中對應的是 increment),代碼如下所示:
~~~
// 第一個頁面,負責讀數據
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 取出資源
final _counter = Provider.of<CounterModel>(context);
return Scaffold(
// 展示資源中的數據
body: Text('Counter: ${_counter.counter}'),
// 跳轉到 SecondPage
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondPage()))
));
}
}
// 第二個頁面,負責讀寫數據
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 取出資源
final _counter = Provider.of<CounterModel>(context);
return Scaffold(
// 展示資源中的數據
body: Text('Counter: ${_counter.counter}'),
// 用資源更新方法來設置按鈕點擊回調
floatingActionButton:FloatingActionButton(
onPressed: _counter.increment,
child: Icon(Icons.add),
));
}
}
~~~
運行代碼,試著多點擊幾次第二個界面的“+”按鈕,關閉第二個界面,可以看到第一個界面也同步到了按鈕的點擊數。
:-: 
圖 1 Provider 使用示例
## Consumer
通過上面的示例可以看到,使用 Provider.of 獲取資源,可以得到資源暴露的數據的讀寫接口,在實現數據的共享和同步上還是比較簡單的。但是,**濫用 Provider.of 方法也有副作用,那就是當數據更新時,頁面中其他的子 Widget 也會跟著一起刷新。**
為驗證這一點,我們以第二個界面右下角 FloatingActionButton 中的子 Widget “+”Icon 為例做個測試。
首先,為了打印出 Icon 控件每一次刷新的情況,我們需要自定義一個控件 TestIcon,并在其 build 方法中返回 Icon 實例的同時,打印一句話:
~~~
// 用于打印 build 方法執行情況的自定義控件
class TestIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("TestIcon build");
return Icon(Icons.add);// 返回 Icon 實例
}
}
~~~
然后,我們用 TestIcon 控件,替換掉 SecondPage 中 FloatingActionButton 的 Icon 子 Widget:
~~~
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 取出共享的數據資源
final _counter = Provider.of<CounterModel>(context);
return Scaffold(
...
floatingActionButton:FloatingActionButton(
onPressed: _counter.increment,
child: TestIcon(),// 替換掉原有的 Icon(Icons.add)
));
}
~~~
運行這段實例,然后在第二個頁面多次點擊“+”按鈕,觀察控制臺輸出:
~~~
I/flutter (21595): TestIcon build
I/flutter (21595): TestIcon build
I/flutter (21595): TestIcon build
I/flutter (21595): TestIcon build
I/flutter (21595): TestIcon build
~~~
可以看到,TestIcon 控件本來是一個不需要刷新的 StatelessWidget,但卻因為其父 Widget FloatingActionButton 所依賴的數據資源 counter 發生了變化,導致它也要跟著刷新。
那么,**有沒有辦法能夠在數據資源發生變化時,只刷新對資源存在依賴關系的 Widget,而其他 Widget 保持不變呢?**
答案當然是可以的。
在本次分享一開始時,我曾說 Provider 可以精確地控制 UI 刷新粒度,而這一切是基于 Consumer 實現的。Consumer 使用了 Builder 模式創建 UI,收到更新通知就會通過 builder 重新構建 Widget。
接下來,我們就看看**如何使用 Consumer 來改造 SecondPage**吧。
在下面的例子中,我們在 SecondPage 中去掉了 Provider.of 方法來獲取 counter 的語句,在其真正需要這個數據資源的兩個子 Widget,即 Text 和 FloatingActionButton 中,使用 Consumer 來對它們進行了一層包裝:
~~~
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
// 使用 Consumer 來封裝 counter 的讀取
body: Consumer<CounterModel>(
//builder 函數可以直接獲取到 counter 參數
builder: (context, CounterModel counter, _) => Text('Value: ${counter.counter}')),
// 使用 Consumer 來封裝 increment 的讀取
floatingActionButton: Consumer<CounterModel>(
//builder 函數可以直接獲取到 increment 參數
builder: (context, CounterModel counter, child) => FloatingActionButton(
onPressed: counter.increment,
child: child,
),
child: TestIcon(),
),
);
}
}
~~~
可以看到,Consumer 中的 builder 實際上就是真正刷新 UI 的函數,它接收 3 個參數,即 context、model 和 child。其中:context 是 Widget 的 build 方法傳進來的 BuildContext,model 是我們需要的數據資源,而 child 則用來構建那些與數據資源無關的部分。在數據資源發生變更時,builder 會多次執行,但 child 不會重建。
運行這段代碼,可以發現,不管我們點擊了多少次“+”按鈕,TestIcon 控件始終沒有發生銷毀重建。
## 多狀態的資源封裝
通過上面的例子,我們學習了 Provider 是如何共享一個數據狀態的。那么,如果有多個數據狀態需要共享,我們又該如何處理呢?
其實也不難。接下來,我就**按照封裝、注入和讀寫這 3 個步驟,與你介紹多個數據狀態的共享**。
在處理多個數據狀態共享之前,我們需要先擴展一下上面計數器狀態共享的例子,讓兩個頁面之間展示計數器數據的 Text 能夠共享 App 傳遞的字體大小。
**首先,我們來看看如何封裝**。
多個數據狀態與單個數據的封裝并無不同,如果需要支持數據的讀寫,我們需要一個接一個地為每一個數據狀態都封裝一個單獨的資源封裝類;而如果數據是只讀的,則可以直接傳入原始的數據對象,從而省去資源封裝的過程。
**接下來,我們再看看如何實現注入。**
在單狀態的案例中,我們通過 Provider 的升級版 ChangeNotifierProvider 實現了可讀寫資源的注入,而如果我們想注入多個資源,則可以使用 Provider 的另一個升級版 MultiProvider,來實現多個 Provider 的組合注入。
在下面的例子中,我們通過 MultiProvider 往 App 實例內注入了 double 和 CounterModel 這兩個資源 Provider:
~~~
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(providers: [
Provider.value(value: 30.0),// 注入字體大小
ChangeNotifierProvider.value(value: CounterModel())// 注入計數器實例
],
child: MaterialApp(
home: FirstPage(),
));
}
}
~~~
在完成了多個資源的注入后,最后我們來看看**如何獲取這些資源**。
這里,我們還是使用 Provider.of 方式來獲取資源。相較于單狀態資源的獲取來說,獲取多個資源時,我們只需要依次讀取每一個資源即可:
~~~
final _counter = Provider.of<CounterModel>(context);// 獲取計時器實例
final textSize = Provider.of<double>(context);// 獲取字體大小
~~~
而如果以 Consumer 的方式來獲取資源的話,我們只要使用 Consumer2 對象(這個對象提供了讀取兩個數據資源的能力),就可以一次性地獲取字體大小與計數器實例這兩個數據資源:
~~~
// 使用 Consumer2 獲取兩個數據資源
Consumer2<CounterModel,double>(
//builder 函數以參數的形式提供了數據資源
builder: (context, CounterModel counter, double textSize, _) => Text(
'Value: ${counter.counter}',
style: TextStyle(fontSize: textSize))
)
~~~
可以看到,Consumer2 與 Consumer 的使用方式基本一致,只不過是在 builder 方法中多了一個數據資源參數。事實上,如果你希望在子 Widget 中共享更多的數據,我們最多可以使用到 Consumer6,即共享 6 個數據資源。
## 總結
好了,今天的分享就到這里,我們總結一下今天的主要內容吧。
我與你介紹了在 Flutter 中通過 Provider 進行狀態管理的方法,Provider 以 InheritedWidget 語法糖的方式,通過數據資源封裝、數據注入和數據讀寫這 3 個步驟,為我們實現了跨組件(跨頁面)之間的數據共享。
我們既可以用 Provider 來實現靜態的數據讀傳遞,也可以使用 ChangeNotifierProvider 來實現動態的數據讀寫傳遞,還可以通過 MultiProvider 來實現多個數據資源的共享。
在具體使用數據時,Provider.of 和 Consumer 都可以實現數據的讀取,并且 Consumer 還可以控制 UI 刷新的粒度,避免與數據無關的組件的無謂刷新。
可以看到,通過 Provider 來實現數據傳遞,無論在單個頁面內還是在整個 App 之間,我們都可以很方便地實現狀態管理,搞定那些通過 StatefulWidget 無法實現的場景,進而開發出簡單、層次清晰、可擴展性高的應用。事實上,當我們使用 Provider 后,我們就再也不需要使用 StatefulWidget 了。
我把今天分享所涉及到的知識點打包到了[GitHub](https://github.com/cyndibaby905/30_provider_demo)中,你可以下載下來,反復運行幾次,加深理解與記憶。
## 思考題
最后,我給你留一道思考題吧。
使用 Provider 可以實現 2 個同樣類型的對象共享,你知道應該如何實現嗎?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略