通過前面這麼多文章的學習,Flutter的狀態管理之路終於要接近尾聲了。ios
其實前面講了這麼多,最後的結論依然是——Provider真香。這畢竟是官方推薦的狀態管理方案,就目前而言,絕大部分的場景均可以使用Provider來進行狀態管理,同時也基本上是最佳方案。git
Google的風格還真是這樣,先不給出任何指定方案,你們百花齊放,最後選一個好一點的改改,這就成了官方方案!
可是咱們爲何還要講這麼多其它的狀態管理方案呢?實際上並很少,你們再去翻閱下前面的文章就能夠發現,我講的都是Flutter中的原生方案,關於第三方的Redux、scope_model等方案,其實我也沒有涉及,其緣由就是但願讀者可以從根本原理上來了解「什麼是狀態管理」、「怎麼進行狀態管理」以及「狀態管理各類方案的優缺點」,只有瞭解了這些,再使用Provider進行狀態管理,就不只僅是調用API這麼簡單了,你會對其根源有所瞭解,這纔是本系列文章的核心所在。github
Provider是Flutter官方提供的狀態管理解決方案,其基本原理是InheritedWidget,Pub地址以下所示。算法
https://github.com/rrousselGit/providerapp
引入 less
Provider的迭代很快,目前最新版本是4.x,在pubspec.yaml中添加Provider的依賴,代碼以下所示。ide
dependencies:
函數
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
學習
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
provider: ^4.3.2+1
執行pub get以後,便可更新Provider庫。ui
Provider的核心實際上就是InheritedWidget,它其實是對InheritedWidget的封裝,讓InheritedWidget在數據管理上可以更加方便的被開發者所使用。
因此,若是你的InheritedWidget比較熟悉,那麼在使用Provider的時候,你必定會有一種似曾相識的感受。
建立DataModel
在使用Provider以前,首先須要對Model進行下處理,經過mixin,爲Model提供notifyListeners的能力。
class TestModel with ChangeNotifier {
int modelValue;
int get value => modelValue;
TestModel({this.modelValue = 0});
void add() {
modelValue++;
notifyListeners();
}
}
在這個Model中,管理了須要共享的數據,同時,提供了修改數據的方法,惟一不同的是,在修改數據後,須要經過ChangeNotifier提供的notifyListeners()來刷新數據。
ChangeNotifierProvider
使用ChangeNotifierProvider,維護須要管理的數據,代碼以下。
class ProviderState1Widget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValue: 1),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ChildWidget1(),
SizedBox(height: 24),
ChildWidget2(),
],
),
),
);
}
}
經過ChangeNotifierProvider的create函數,建立初始化的Model。同時建立其Child,這個風格和InheritedWidget是否是有殊途同歸之妙。
Provider提供了不少不一樣類型的Provider,這裏先只用瞭解ChangeNotifierProvider管理數據之Provider.of
經過Provider管理的數據,能夠經過Provider.of(context);來讀取數據,代碼以下所示。
var style = TextStyle(color: Colors.white);
class ChildWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidget1 build');
var model = Provider.of(context);
return Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Text('Model data: ${model.value}', style: style),
RaisedButton(
onPressed: () => model.add(),
child: Text('add'),
),
],
),
);
}
}
還能夠經過model來獲取操做數據的方法add()。
效果如圖所示。
這樣就完成了一個最簡單的Provider使用方法。
可是經過日誌能夠發現,每次調用Provider.of(context);後,都會致使Context所處的Widget執行Rebuild。
I/flutter (18490): ChildWidget2 build
I/flutter (18490): ChildWidget1 build
是否是又似曾相識?是的,這就是前面文章中所提到的dependOnInheritedWidgetOfExactType的問題,它會對調用者進行記錄,在數據更新時,對數據進行rebuild操做。
另外,上面的例子中,實際上還隱藏了一個很容易被初學者忽視的問題,咱們來看下這段代碼。
RaisedButton(
onPressed: () => model.add(),
child: Text('add'),
),
在button的點擊事件中,咱們並無直接使用每次調用Provider.of(context).add(),而是將每次調用Provider.of(context)抽取了出來,爲何要畫蛇添足呢?
其實你們能夠嘗試下這樣調用,點擊後,會報錯,以下所示。
Tried to listen to a value exposed with provider, from outside of the widget tree.
This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing `listen: false`.
To fix, write:
Provider.of(context, listen: false);
It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn't care about the value.
簡單的說,就是在button的event handler中,觸發了Provider.of,可是這個時候,傳入的Context並不在Widget中,致使notifyListeners出錯。
解決方法有兩個,一個就是將Provider.of抽取出來,用Widget的Context來獲取Model,另外一個呢,就是經過Provider.of的另外一個參數來去掉監聽的註冊。
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
經過listen: false,去掉默認註冊的監聽。
Provider.of的默認實現中,listen = true,至於爲何,你們能夠看這裏的討論。https://github.com/rrousselGit/provider/issues/188#issuecomment-526259839 https://github.com/rrousselGit/provider/issues/313#issuecomment-576156922
所以,咱們總結了兩條Provider的使用規則。
Provider.of(context):用於須要根據數據的變化而自動刷新的場景
Provider.of(context, listen: false):用於只須要觸發Model中的操做而不關心刷新的場景
所以對應的,在新版本的Provider中,做者還提供了兩個Context的拓展函數,來進一步簡化調用。
T watch()
T read()
他們就分別對應了上面的兩個使用場景,因此在上面的示例中,Text獲取數據的方式,和在Button中點擊的方式還能夠寫成下面這張形式。
Text('watch: ${context.watch().value}', style: style)
RaisedButton(
onPressed: () => context.read().add(),
child: Text('add'),
),
代碼地址 Flutter Dojo-Backend-ProviderState1Widget管理數據之Consumer
獲取Provider管理的數據Model,有兩種方式,一種是經過Provider.of(context)來獲取,另外一種,是經過Consumer來獲取,在設計Consumer時,做者給它賦予了兩個功能。
當傳入的BuildContext中,不存在指定的Provider時,Consumer容許咱們從Provider中的獲取數據(其緣由就是Provider使用的是InheritedWidget,因此只能遍歷父Widget,當指定的Context對應的Widget與Provider處於同一個Context時,就沒法找到指定的InheritedWidget了)
提供更加精細的數據刷新範圍,避免無謂的刷新
建立新的Context環境
首先,咱們來看下第一個功能。
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValue: 1),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Text('Model data: ${Provider.of(context).value}', style: style),
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
],
),
),
),
),
);
}
在上面的這個例子中,ChangeNotifierProvider和使用Provider的Widget,使用的是同一個Context,因此確定是沒法找到對應的InheritedWidget的,因此會報錯。
The following ProviderNotFoundException was thrown building ProviderState2Widget(dirty):
Error: Could not find the correct Provider above this ProviderState2Widget Widget
This likely happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that ProviderState2Widget is under your MultiProvider/Provider.
This usually happen when you are creating a provider and trying to read it immediately.
解決方法也很簡單,一個是將須要使用Provider的Widget抽取出來,放入一個新的Widget中,這樣在這個Widget中,就有了屬於本身的Context,另外一種,就是經過Consumer,來建立一個新的Context環境,代碼以下所示。
@override
控制更加精細的刷新範圍
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValue: 1),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.redAccent,
height: 48,
child: Consumer(
builder: (BuildContext context, value, Widget child) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Text('Model data: ${value.value}', style: style),
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
],
);
},
),
),
),
),
);
}
來看下下面這個例子。
class ProviderState2Widget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValue: 1),
child: NewWidget(),
);
}
}
class NewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Text('Model data: ${Provider.of(context).value}', style: style),
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
],
),
),
),
);
}
}
在調用Provider.of的時候,會形成Context範圍內的Widget執行Rebuild,這個在前面的例子中,已經看過了。那麼要解決這個問題,也很簡單,只須要將須要刷新的Widget,用Consumer包裹便可,這樣在收到notifyListeners時,就只有Consumer範圍內的Widget會進行刷新了,其它範圍的地方,就不會被迫刷新了。在Consumer的builder中,能夠獲取指定泛型的數據對象,代碼以下所示。
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Consumer(
builder: (BuildContext context, value, Widget child) {
return Text(
'Model data: ${value.value}',
style: style,
);
},
),
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
],
),
),
),
);
}
代碼地址 Flutter Dojo-Backend-ProviderState2Widget
那麼Consumer到底是爲何能夠實現更加精細的刷新控制呢?其實原理很簡單,前面甚至已經提到了,那就是「在調用Provider.of的時候,會形成Context範圍內的Widget執行Rebuild」,因此,只須要將調用的範圍儘量的縮小,那麼執行Rebuild的範圍就會越小,看下Consumer的源碼。
能夠發現,Consumer就是經過一個Builder,來進行了一層封裝,最終仍是調用的Provider.of(context), 看了源碼以後,相信你們應該能理解Consumer的這兩個功能了。
more Consumer
Consumer中存在多個類型的變種,它表明着使用多個數據模型的數據獲取方式,如圖所示。
其實說簡單點,就是在一個Consumer的builder中,同時獲取多個不一樣類型的數據模型,是一種簡單的寫法,是一種將嵌套的過程打平的過程。源碼中只寫到Consumer6,即支持同時最多6個數據類型,若是要支持更多,則須要本身實現了。
管理數據之Selector
Selector一樣是獲取數據的一種方式,從理論上來講,Selector等於Consumer等於Provider.of,可是它們對數據的控制粒度,纔是它們之間根本的區別。
獲取數據的方式,從Provider.of,到Consumer,再到Selector,實際上經歷了這樣一種進化。
Provider.of:Context內容進行Rebuild
Consumer:Model內容變化進行Rebuild
Selector:Model中的指定內容變化進行Rebuild
能夠發現,雖然都是獲取數據,可是其控制的精細程度確是遞增的。
下面就經過一個例子,來演示下Selector的使用場景。
首先,咱們定義一個數據模型,代碼以下所示。
class TestModel with ChangeNotifier {
int modelValueA;
int modelValueB;
int get valueA => modelValueA;
int get valueB => modelValueB;
TestModel({this.modelValueA = 0, this.modelValueB = 0});
void addA() {
modelValueA++;
notifyListeners();
}
void addB() {
modelValueB++;
notifyListeners();
}
}
在這個數據模型中,管理了兩個類型的數據,modelValueA和modelValueB。
下面是展現界面。
class ProviderState3Widget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValueA: 1, modelValueB: 1),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ChildWidgetA(),
SizedBox(height: 24),
ChildWidgetB(),
],
),
),
);
}
}
var style = TextStyle(color: Colors.white);
class ChildWidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidgetA build');
var model = Provider.of(context);
return Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('ChildA', style: style),
Text('Model data: ${model.valueA}', style: style),
RaisedButton(
onPressed: () => model.addA(),
child: Text('add'),
),
],
),
);
}
}
class ChildWidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidgetB build');
var model = Provider.of(context);
return Container(
color: Colors.blueAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('ChildB', style: style),
Text('Model data: ${model.valueB}', style: style),
RaisedButton(
onPressed: () => model.addB(),
child: Text('add'),
),
],
),
);
}
}
效果如圖所示。
在上面的代碼下,不論咱們點擊ChildA的Add,仍是ChildB的Add,整個界面都會Rebuild。即便經過Consumer,也沒法作到只刷新對應的數據,緣由在於它們的數據模型是同一個,Consumer只能作到數據模型層面上的更新刷新,可是沒法針對同一個數據模型中不一樣字段的變換而進行更新。
因此,Consumer解決方案就是須要將這個數據模式拆成兩個,ModelA和ModelB,這樣使用MultiProvider管理ChangeNotifierProvider(ModleA)和ChangeNotifierProvider(ModelB),再經過Consumer分別管理ModelA和ModelB,這樣才能作到互補干擾的刷新。
那若是數據模型不能拆分呢?這個時候,就可使用Selector了,先來看下Selector的構造函數。
A表明傳入的數據源,例如前面的TestModel
S表明想要監聽的A數據源中的的某個屬性,好比TestModel的ModelA
selector的功能,就是從A數據源中篩選出須要監聽的數據S,而後將S傳遞傳給builder進行構造
shouldRebuild用來覆蓋默認的對比算法,能夠不設置
對比算法以下所示。
從源碼能夠發現,Selector判斷的標準就是新舊數據Model是否「==」,若是是Collection類型,則經過DeepCollectionEquality來進行比較,官方建議使用https://pub.flutter-io.cn/packages/tuple 來進行簡化判斷
有了Selector以後,就能夠在同一個數據模型中,根據條件,篩選出不一樣的刷新條件了,這樣就能夠避免數據模型中的某個屬性變換而引發的整個數據模型刷新了。
經過Selector,將上面的代碼進行下改造。
class ChildWidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidgetA build');
return Selector(
selector: (context, value) => value.modelValueA,
builder: (BuildContext context, value, Widget child) {
return Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('ChildA', style: style),
Text('Model data: $value', style: style),
RaisedButton(
onPressed: () => context.read().addA(),
child: Text('add'),
),
],
),
);
},
);
}
}
這樣經過Selector進行一次篩選,就能夠避免同一個Model中不一樣的數據刷新致使整個Model Rebuild的問題,例如上面的Selector,指定了須要在TestModel中尋找int類型的數據,其過濾條件是TestModel中的modelValueA這樣一個int類型的數據,根據ShouldRebuild(默認實現)的判斷,返回這個狀況下,ChildWidgetA是否須要Rebuild。
與Provider.of相似,在4.1以後,Provider提供了基於BuildContext的拓展函數來簡化Selector的使用,例如上面的代碼經過selector拓展函數來實現,代碼以下所示。
class ChildWidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidgetB build');
return Container(
color: Colors.blueAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('ChildB', style: style),
Builder(
builder: (BuildContext context) {
return Text(
'Model data: ${context.select((TestModel value) => value.modelValueB)}',
style: style,
);
},
),
RaisedButton(
onPressed: () => context.read().addB(),
child: Text('add'),
),
],
),
);
}
}
不過須要注意的是,這裏須要經過Builder來建立一個子類的Context,避免當前Context的刷新。
more Selector
與Consumer相似,Selector一樣也有多種不一樣的實現。
其實很簡單,就是實現多種不一樣的數據類型,在這些數據模型中,找到須要監聽的那一種類型,這種狀況比較經常使用於多個數據模型中具體共同參數的場景。
上面就是經過Provider來獲取被管理的數據的三種方式:Provider.of,Consumer和Selector,它們的功能徹底一致,區別僅僅在於刷新的控制粒度。