在flutter
中狀態(tài)管理是重中之重趁舀,每當(dāng)談這個(gè)話題,總有說(shuō)不完的話惫霸。
在正式介紹 Provider
為什么我們需要狀態(tài)管理。如果你已經(jīng)對(duì)此十分清楚拄衰,那么建議直接跳過(guò)這一節(jié)它褪。
如果我們的應(yīng)用足夠簡(jiǎn)單饵骨,Flutter
作為一個(gè)聲明式框架翘悉,你或許只需要將 數(shù)據(jù) 映射成 視圖 就可以了。你可能并不需要狀態(tài)管理居触,就像下面這樣妖混。
但是隨著功能的增加老赤,你的應(yīng)用程序?qū)?huì)有幾十個(gè)甚至上百個(gè)狀態(tài)。這個(gè)時(shí)候你的應(yīng)用應(yīng)該會(huì)是這樣制市。
這又是什么鬼抬旺。我們很難再清楚的測(cè)試維護(hù)我們的狀態(tài),因?yàn)樗瓷先?shí)在是太復(fù)雜了祥楣!而且還會(huì)有多個(gè)頁(yè)面共享同一個(gè)狀態(tài)开财,例如當(dāng)你進(jìn)入一個(gè)文章點(diǎn)贊,退出到外部縮略展示的時(shí)候误褪,外部也需要顯示點(diǎn)贊數(shù)责鳍,這時(shí)候就需要同步這兩個(gè)狀態(tài)。
Flutter
實(shí)際上在一開(kāi)始就為我們提供了一種狀態(tài)管理方式兽间,那就是 StatefulWidget
历葛。但是我們很快發(fā)現(xiàn),它正是造成上述原因的罪魁禍?zhǔn)住?br>
在 State
屬于某一個(gè)特定的 Widget
嘀略,在多個(gè) Widget
之間進(jìn)行交流的時(shí)候恤溶,雖然你可以使用 callback
解決,但是當(dāng)嵌套足夠深的話帜羊,我們?cè)黾臃浅6嗫膳碌睦a咒程。這時(shí)候,我們便迫切的需要一個(gè)架構(gòu)來(lái)幫助我們理清這些關(guān)系逮壁,狀態(tài)管理框架應(yīng)運(yùn)而生孵坚。
Provider 是什么
通過(guò)使用
Provider
而不用手動(dòng)編寫(xiě)InhertedWidget,您將獲取自動(dòng)分配窥淆、延遲加載卖宠、大大減少每次創(chuàng)建新類(lèi)的代碼。
首先在yaml
中添加,具體版本號(hào)參考:官方Provider pub忧饭,當(dāng)前版本號(hào)是4.1.3
.
Provider: ^4.1.3
然后運(yùn)行
flutter pub get
獲取到最新的包到本地扛伍,在需要的文件夾內(nèi)導(dǎo)入
import 'package:provider/provider.dart';
簡(jiǎn)單例子
我們還用點(diǎn)擊按鈕新增數(shù)字的例子
首先創(chuàng)建存儲(chǔ)數(shù)據(jù)的Model
class ProviderModel extends ChangeNotifier {
int _count=0;
ProviderModel();
void plus() {
/// 在數(shù)據(jù)變動(dòng)的時(shí)候通知監(jiān)聽(tīng)者刷新UI
_count = _count + 1;
notifyListeners();
}
}
構(gòu)造view
/// 使用Consumer來(lái)監(jiān)聽(tīng)全局刷新UI
Consumer<ProviderModel>(
builder:
(BuildContext context, ProviderModel value, Widget child) {
print('Consumer 0 刷新');
_string += 'c0 ';
return _Row(
value: value._count.toString(),
callback: () {
context.read<ProviderModel>().plus();
},
);
},
child: _Row(
value: '0',
callback: () {
context.read<ProviderModel>().plus();
},
),
)
測(cè)試下看下效果:
單個(gè)Model多個(gè)小部件分別刷新(局部刷新)
單個(gè)model
實(shí)現(xiàn)單個(gè)頁(yè)面多個(gè)小部件分別刷新,是使用Selector<Model,int>
來(lái)實(shí)現(xiàn)词裤,首先看下構(gòu)造函數(shù):
class Selector<A, S> extends Selector0<S> {
/// {@macro provider.selector}
Selector({
Key key,
@required ValueWidgetBuilder<S> builder,
@required S Function(BuildContext, A) selector,
ShouldRebuild<S> shouldRebuild,
Widget child,
}) : assert(selector != null),
super(
key: key,
shouldRebuild: shouldRebuild,
builder: builder,
selector: (context) => selector(context, Provider.of(context)),
child: child,
);
}
可以看到Selector
繼承了Selector0
,再看Selector
關(guān)鍵build
代碼:
class _Selector0State<T> extends SingleChildState<Selector0<T>> {
T value;
Widget cache;
Widget oldWidget;
@override
Widget buildWithChild(BuildContext context, Widget child) {
final selected = widget.selector(context);
var shouldInvalidateCache = oldWidget != widget ||
(widget._shouldRebuild != null &&
widget._shouldRebuild.call(value, selected)) ||
(widget._shouldRebuild == null &&
!const DeepCollectionEquality().equals(value, selected));
if (shouldInvalidateCache) {
value = selected;
oldWidget = widget;
cache = widget.builder(
context,
selected,
child,
);
}
return cache;
}
}
根據(jù)我們傳入的_shouldRebuild
來(lái)判斷是否需要更新刺洒,如果需要更新則執(zhí)行widget.build(context,selected,child)
,否則返回已經(jīng)緩存的cache
.當(dāng)沒(méi)有_shouldRebuild
參數(shù)時(shí)則根據(jù)widget.selector(ctx)
的返回值判斷是否和舊值相等,不等則更新UI
吼砂。
所以我們不寫(xiě)shouldRebuild
也是可以的逆航。
局部刷新用法
Widget build(BuildContext context) {
print('page 1');
_string += 'page ';
return Scaffold(
appBar: AppBar(
title: Text('Provider 全局與局部刷新'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text('全局刷新<Consumer>'),
Consumer<ProviderModel>(
builder:
(BuildContext context, ProviderModel value, Widget child) {
print('Consumer 0 刷新');
_string += 'c0 ';
return _Row(
value: value._count.toString(),
callback: () {
context.read<ProviderModel>().plus();
},
);
},
child: _Row(
value: '0',
callback: () {
context.read<ProviderModel>().plus();
},
),
),
SizedBox(
height: 40,
),
Text('局部刷新<Selector>'),
Selector<ProviderModel, int>(
builder: (ctx, value, child) {
print('Selector 1 刷新');
_string += 's1 ';
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Selector<Model,int>次數(shù):' + value.toString()),
OutlineButton(
onPressed: () {
context.read<ProviderModel>().plus2();
},
child: Icon(Icons.add),
)
],
);
},
selector: (ctx, model) => model._count2,
shouldRebuild: (m1, m2) {
print('s1:$m1 $m2 ${m1 != m2 ? '不相等,本次刷新' : '數(shù)據(jù)相等渔肩,本次不刷新'}');
return m1 != m2;
},
),
SizedBox(
height: 40,
),
Text('局部刷新<Selector>'),
Selector<ProviderModel, int>(
selector: (context, model) => model._count3,
shouldRebuild: (m1, m2) {
print('s2:$m1 $m2 ${m1 != m2 ? '不相等因俐,本次刷新' : '數(shù)據(jù)相等,本次不刷新'}');
return m1 != m2;
},
builder: (ctx, value, child) {
print('selector 2 刷新');
_string += 's2 ';
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Selector<Model,int>次數(shù):' + value.toString()),
OutlineButton(
onPressed: () {
ctx.read<ProviderModel>().plus3();
},
child: Icon(Icons.add),
)
],
);
},
),
SizedBox(
height: 40,
),
Text('刷新次數(shù)和順序:↓'),
Text(_string),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlineButton(
child: Icon(Icons.refresh),
onPressed: () {
setState(() {
_string += '\n';
});
},
),
OutlineButton(
child: Icon(Icons.close),
onPressed: () {
setState(() {
_string = '';
});
},
)
],
)
],
),
),
);
}
效果:
當(dāng)我們點(diǎn)擊局部刷新s1
,執(zhí)行s1
的build
抹剩,s1
不相等撑帖,s2
相等不刷新。輸出:
flutter: s2:5 5 數(shù)據(jù)相等澳眷,本次不刷新
flutter: s1:6 7 不相等胡嘿,本次刷新
flutter: Selector 1 刷新
flutter: Consumer 0 刷新
當(dāng)點(diǎn)擊s2
,s2
的值不相等刷新UI
,s1
數(shù)據(jù)相等,不刷新UI
.
flutter: s2:2 3 不相等钳踊,本次刷新
flutter: selector 2 刷新
flutter: s1:0 0 數(shù)據(jù)相等衷敌,本次不刷新
flutter: Consumer 0 刷新
可以看到上邊2次Consumer
每次都刷新了,我們探究下原因拓瞪。
Consumer 全局刷新
Consumer
繼承了SingleCHildStatelessWidget
,當(dāng)我們?cè)?code>ViewModel中調(diào)用notification
則當(dāng)前widget
被標(biāo)記為dirty
,然后在build
中執(zhí)行傳入的builder
函數(shù)逢享,在下幀則會(huì)刷新UI
。
而Selector<T,S>
則被標(biāo)記dirty
時(shí)執(zhí)行_Selector0State
中的buildWithChild(ctx,child)
函數(shù)時(shí)吴藻,根據(jù)selected
和_shouldRebuild
來(lái)判斷是否需要執(zhí)行widget.builder(ctx,selected,child)
(刷新UI
).
其他用法
多model寫(xiě)法
只需要在所有需要model
的上級(jí)包裹即可瞒爬,當(dāng)我們一個(gè)page
需要2
個(gè)model
的時(shí)候,我么通常這樣子寫(xiě):
class BaseProviderRoute extends StatelessWidget {
BaseProviderRoute({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProviderModel>(
create: (_) => ProviderModel(),
),
ChangeNotifierProvider<ProviderModel2>(create: (_) => ProviderModel2()),
],
child: BaseProvider(),
);
}
}
當(dāng)然是用的時(shí)候和單一model
一致的沟堡。
Selector<ProviderModel2, int>(
selector: (context, model) => model.value,
builder: (ctx, value, child) {
print('model2 s1 刷新');
_string += 'm2s1 ';
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Selector<Model2,int>次數(shù):' + value.toString()),
OutlineButton(
onPressed: () {
ctx.read<ProviderModel2>().add(2);
},
child: Icon(Icons.add),
)
],
);
},
),
watch && read
watch
源碼是Provider.of<T>(this)
,默認(rèn)Provider.of<T>(this)
的listen=true
.
static T of<T>(BuildContext context, {bool listen = true}){
final inheritedElement = _inheritedElementOf<T>(context);
if (listen) {
context.dependOnInheritedElement(inheritedElement);
}
return inheritedElement.value;
}
而read
源碼是Provider.of<T>(this, listen: false)
,watch
/read
只是寫(xiě)法簡(jiǎn)單一點(diǎn)侧但,并無(wú)高深結(jié)構(gòu)。
當(dāng)我們想要監(jiān)聽(tīng)值的變化則是用
watch
,當(dāng)想調(diào)用model
的函數(shù)時(shí)則使用read