好久以前,在咱們的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
的狀態管理方式包括但不限於Provider
,Bloc
,Redux
以及Fish-Redux
。less
Bloc
準確地來講是一種理念,也是我使用的第一個狀態管理,如今也有對應的實現庫,通常來講是基於響應式編程的。Redux
對於React
開發者來講並不陌生,畢竟Flutter
這塊也是借鑑了React
。Fish-Redux
脫胎於Redux
,阿里出品,整體來講比較複雜,適合中大型項目。如今社會也有生成Fish-Redux
模板代碼的工具Provider
是Google
推薦的狀態管理,也是我使用的第二種狀態管理,相對來講比較簡單省心。接下來,我將簡單地介紹一下Provider
的使用。ide
Provider
實際上是對InheritedWidget的封裝。相比於直接使用InheritedWidget
,使用Provider
有不少好處,好比說簡化資源的分配與處置,支持懶加載等等。工具
Provider
爲咱們提供了一些不一樣類型的Provider
。要查看全部類型的provider
能夠點擊這裏佈局
name | description |
---|---|
Provider | 最基礎的provider。它攜帶一個值並將這個值暴露,不管這個值是什麼。 |
ListenableProvider | 爲Listenable 對象而建立的provider 。ListenableProvider 會監聽對象的變化,只要ListenableProvider 的listner被調用,ListenableProvider 就會從新構建依賴於該provider的控件。 |
ChangeNotifierProvider | ChangeNotifierProvider 是一種特殊的ListenableProvider ,它基於ChangeNotifier ,而且在有須要的時候,它會自動調用ChangeNotifier.dispose 。 |
ValueListenableProvider | 監聽ValueListenable 並只會暴露ValueListenable.value . |
StreamProvider | 監聽一個Stream 而且對外暴露最新提交的值。 |
FutureProvider | 攜帶一個 Future ,當Future 完成時,它會更新依賴於它的控件。 |
鑑於本人才疏學淺,本文並不會逐一講述如何使用各類Provider
,因此本文挑選了我用的最多的ChangeNotifierProvider
來說解,但願能夠拋磚引玉。
通常來講建立Provider
有兩種方式:
.value
構造方法當咱們要新建立一個對象,咱們要使用默認構造方法而不是使用.value
構造方法,由於若是咱們經過.value
建立一個對象可能會引發內存泄漏或產生一些意想不到的問題。 這裏簡單解釋一下爲何不能使用value
建立一個對象,英文好的能夠看StackOverflow原文。由於Flutter
中的build
方法應該是純淨無反作用的,不少外部因素會觸發rebuild
,好比說:
InheritedWidget
的控件(Class.of(context) 部分)發生了變化因此說,使用.value
建立對象的問題在於會使得build
變得不純粹或者說具備反作用,會使來自外部的build
調用變得很麻煩。 這個問題到此爲止,喜歡研究的朋友能夠自行探索。
Provider
的create
中建立對象。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: ...
)
複製代碼
筆記:當使用
Provider
的create/update
回調時,咱們要注意的是,默認狀況下,create/update
的調用是懶式調用的。這就意味着,只有咱們Provider
中的數據至少被請求一次,create/update
纔會被調用。若是咱們想作一些預處理,咱們可使用lazy
參數來禁止這一特性:
MyProvider(
create: (_) => Something(),
lazy: false,
)
複製代碼
最簡單的讀取數據的方式是使用BuildContext
的擴展方法:
固然了咱們也可使用靜態方法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)。 實際上,這並不會在控件樹中游走。
說到如今,無非仍是對文檔的翻譯,如今讓咱們走碼上任吧~~~
故事仍是要從Flutter
的計數器提及,由於新建立的Flutter
項目模板就是這個計數器了,如今咱們要用ChangeNotifierProvider
來簡單改造一下這個項目。
MyHomePage
由StatefulWidget
改爲StatelessWidget
。ChangeNotifierProvider
來更新頁面首先,咱們要建立一個ChangeNotifier
:
class MyChangeNotifier extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
incrementCounter() {
_counter++;
notifyListeners();//要更新UI記得調用這個方法
}
}
複製代碼
當前咱們點擊FloatingActionButton
時會調用MyChangeNotifier
的incrementCounter
方法,要注意的是當咱們處理完業務時,若是須要更新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。
未完待續。。。 期待不期待你說了算。