前言
今天偶然發(fā)現(xiàn)在谷歌爸爸的倉庫下出現(xiàn)了一個叫做flutter-provide的狀態(tài)管理框架盏筐,2月8日才第一次提交,非常新鮮。在簡單上手之后感覺就是一個字——爽偶洋!所以今天就跟大家分享一下這個新的狀態(tài)管理框架送讲。
Provider被設(shè)計為ScopedModel的替代品,并且允許我們更加靈活地處理數(shù)據(jù)類型和數(shù)據(jù)。但是首先呢還是先說說老生常談的狀態(tài)管理漾抬。
為什么需要狀態(tài)管理
在我們一開始構(gòu)建應(yīng)用的時候,也許很簡單岂座。我們有一些狀態(tài)陶因,直接把他們映射成視圖就可以了。這種簡單應(yīng)用可能并不需要狀態(tài)管理巷嚣。
但是隨著功能的增加喘先,你的應(yīng)用程序?qū)袔资畟€甚至上百個狀態(tài)。這個時候你的應(yīng)用應(yīng)該會是這樣廷粒。
Wow窘拯,這是什么鬼。我們很難再清楚的測試維護(hù)我們的狀態(tài)坝茎,因為它看上去實在是太復(fù)雜了涤姊!而且還會有多個頁面共享同一個狀態(tài),例如當(dāng)你進(jìn)入一個文章點贊嗤放,退出到外部縮略展示的時候思喊,外部也需要顯示點贊數(shù),這時候就需要同步這兩個狀態(tài)次酌。
這時候恨课,我們便迫切的需要一個架構(gòu)來幫助我們理清這些關(guān)系舆乔,狀態(tài)管理框架應(yīng)運(yùn)而生。
什么是Provide
和Scoped_model一樣剂公,Provide也是借助了InheritWidget希俩,將共享狀態(tài)放到頂層MaterialApp之上。底層部件通過Provier獲取該狀態(tài)纲辽,并通過混合ChangeNotifier通知依賴于該狀態(tài)的組件刷新颜武。
Provide還提供了Provide.stream,讓我們能夠以處理流的方式處理數(shù)據(jù)文兑,不過目前還有一些問題盒刚,不推薦使用。
Lets do it绿贞!
我們這里還是以一個簡單app為例因块,詳細(xì)介紹Provide的用法。其中涉及共享狀態(tài)以及多個狀態(tài)之間如何管理籍铁。
這兩個頁面都同時依賴于counter 和 switcher兩個不同的狀態(tài)涡上。并且一個頁面改變狀態(tài)之后另外一個頁面狀態(tài)也隨之改變。
該項目完整代碼已放在 Github
第一步:添加依賴
在pubspec.yaml中添加Provide的依賴拒名。
- 實際添加請參考:https://pub.dartlang.org/packages/provide#-installing-tab-
- 由于版本沖突添加失敗請參考: https://juejin.im/post/5b8958d351882542b03e6d57
第二步:創(chuàng)建Model
這里實際上它承擔(dān)了State的職責(zé)吩愧,但是為了和官方的State區(qū)分所以叫做model。
import 'package:flutter/material.dart';
class Counter with ChangeNotifier{
int value = 0;
increment(){
value++;
notifyListeners();
}
}
這里我們可以看到增显,數(shù)據(jù)和操作數(shù)據(jù)的方法都在model中雁佳,我們可以很清晰的把業(yè)務(wù)分離出來。
對比Scoped_model可以發(fā)現(xiàn)同云,Provide模式中model不再需要繼承Model類糖权,只需要實現(xiàn)Listenable,我們這里混入ChangeNotifier炸站,可以不用管理聽眾星澳。
通過 notifyListeners 我們可以通知聽眾刷新。
第三步:將狀態(tài)放入頂層
void main() {
var counter = Counter();
var providers = Providers();
//將counter對象添加進(jìn)providers
providers.provide(Provider<Counter>.value(counter));
runApp(
ProviderNode(
child: MyApp(),
providers: providers),
);
}
ProviderNode封裝了InheritWidget旱易,并且提供了
一個providers容器用于放置狀態(tài)禁偎。
ProviderScope 為Provider提供單獨(dú)的類型空間,它允許多個相同類型的提供者阀坏。默認(rèn)使用ProviderScope('_default'),存放的時候你可以通過ProviderScope("name")來指定key如暖。
添加一組Provider的時候建議使用provideFrom或者provide方法,而不是provideAll忌堂,因為它可以檢查編譯時的類型錯誤装处。
Provider<Counter>.value將counter包裝成了_ValueProvider。并在它的內(nèi)部提供了StreamController從而實現(xiàn)對數(shù)據(jù)進(jìn)行流式操作。
第四步:獲取狀態(tài)
同樣的Provide也提供了兩種獲取State的方法妄迁。我們先來介紹第一種寝蹈,通過Provide<T>小部件獲取。
Provide<Counter>(
builder: (context, child, counter) {
return Text(
'${counter.value}',
style: Theme.of(context).textTheme.display1,
);
},
),
每次通知數(shù)據(jù)刷新時登淘,builder將會重新構(gòu)建這個小部件箫老。
builder方法接收三個參數(shù),這里主要介紹第二個和第三個黔州。
- 第二個參數(shù)child:假如這個小部件足夠復(fù)雜耍鬓,內(nèi)部有一些小部件是不會改變的,那么我們可以將這部分小部件寫在Provide的child屬性中流妻,讓builder不再重復(fù)創(chuàng)建這些小部件牲蜀,以提升性能。
- 第三個參數(shù)counter:這個參數(shù)代表了我們獲取的頂層providers中的狀態(tài)<T>绅这。
scope:通過指定ProviderScope獲取該鍵所對應(yīng)的狀態(tài)涣达。在需要使用多個相同類型狀態(tài)的時候使用。
第二種獲取方式:Provide.value<T>(context)
final currentCounter = Provide.value<Counter>(context);
這種方式實際上調(diào)用了context.inheritFromWidgetOfExactType找到頂層的_InheritedProviders來獲取到頂層providers中的狀態(tài)证薇。
如何組織多個狀態(tài)
和scoped_model不同的是度苔,provide模式中你可以輕松組織多個狀態(tài)。只需要將狀態(tài)provide進(jìn)provider中就可以了浑度。
void main() {
var counter = Counter();
var switcher = Switcher();
var providers = Providers();
providers
..provide(Provider<Counter>.value(counter))
..provide(Provider<Switcher>.value(switcher));
runApp(
ProviderNode(
child: MyApp(),
providers: providers)
);
}
獲取數(shù)據(jù)流
在將counter添加進(jìn)providers的過程中進(jìn)行了一次包裝寇窑。我們剛才通過分析源碼知道了這個操作能夠讓我們處理流式數(shù)據(jù)。
通過 Provide.stream<T>(context) 就能獲取數(shù)據(jù)流箩张。需要注意的是甩骏,這里每次獲取的數(shù)據(jù)流都
StreamBuilder<Counter>(
initialData: currentCounter,
stream: Provide.stream<Counter>(context)
.where((counter) => counter.value % 2 == 0),
builder: (context, snapshot) =>
Text('Last even value: ${snapshot.data.value}')),
不過在我的使用當(dāng)中出現(xiàn)了streamTransformer失效的情況。在firstScreen和secondScreen同樣應(yīng)用這一段相同的代碼先慷,second screen的where方法能夠生效横漏,過濾掉奇數(shù)數(shù)據(jù),而first screen中則是收到了完整的數(shù)據(jù)熟掂。
需要注意的是,這里每次獲取的數(shù)據(jù)流都會重新創(chuàng)建一條新的流扎拣。
/// Creates a provider that listens to a stream and caches the last
/// received value of the stream.
/// This provider notifies for rebuild after every release.
factory Provider.stream(Stream<T> stream, {T initialValue}) =>
_StreamProvider<T>(stream, initialValue: initialValue);
關(guān)于這個做法還有一些爭議赴肚,具體可以查看這個issue:
https://github.com/google/flutter-provide/issues/3
不過這個功能還可以結(jié)合rxdart使用,可以通過stream輕松構(gòu)建Observer二蓝,讓我們更加靈活的組織數(shù)據(jù)誉券。
根據(jù)多個狀態(tài)重建小部件
當(dāng)我們一個視圖可能依賴于多個狀態(tài)進(jìn)行重建的時候,可以使用ProvideMulti小部件刊愚。
已知的坑
Provide.stream 詭異的手動監(jiān)聽
由于 Provide 自動將 Listenable 數(shù)據(jù)包裝并提供了 Provide.stream 接口踊跟,讓我們可以通過監(jiān)聽這個流,來獲取最新事件。但是當(dāng)我們進(jìn)行手動監(jiān)聽之后將會發(fā)生這件詭異的事情商玫。
···
class _SecondScreenState extends State<SecondScreen> {
StreamSubscription<Switcher> _subscription;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_subscription = Provide.stream<Switcher>(context).listen((data){
print(data.toString());
});
}
@override
void dispose() {
unSubscribe();
super.dispose();
}
unSubscribe(){
if(_subscription != null){
_subscription.cancel();
_subscription = null;
}
}
···
按理說這里應(yīng)該在數(shù)據(jù)發(fā)生變化的時候收到一條事件箕憾,可是我們這里發(fā)現(xiàn)一次性輸出了 5 條 flutter: Instance of 'Switcher'。
為什么是 5 條呢拳昌,這是因為我一共在 5 處 地方收聽過這個數(shù)據(jù)袭异,包括使用 Provide Widget 也算一次收聽。
而當(dāng)我退出第二個頁面之后再次進(jìn)入炬藤,發(fā)現(xiàn)這次收到的數(shù)據(jù)比上次多了 5 條御铃。
出現(xiàn)這個現(xiàn)象是由于這個 stream 是由工廠方法創(chuàng)建,每次調(diào)用 Provide.stream 都會重新創(chuàng)建出來一條流沈矿。就算收聽者不再收聽上真,這條流也會存在。
所以不要去手動監(jiān)聽你的 Provide.stream羹膳。
寫在最后
自從上次寫完狀態(tài)管理拓展篇Rxdart之后斷更了三個月睡互。總結(jié)篇遲遲沒有出來溜徙,在這里先說一聲抱歉湃缎。對于我來講狀態(tài)管理這個本身就是一個新鮮玩意,所以在沒有經(jīng)過大型應(yīng)用實戰(zhàn)檢驗的總結(jié)都是空談蠢壹。
這也是為什么我遲遲沒有開始寫總結(jié)篇的原因嗓违。不過在這我可以說一些自己的感受,供大家參考图贸。
在這幾個月中蹂季,我用的比較多的是BLoC,它組織數(shù)據(jù)確實非常靈活疏日,可以很輕松的實現(xiàn)懶加載之類的操作偿洁。而且stateful widget寫的是越來越少了。缺點就是入門的門檻比較高沟优,理解StreamTransformer和為什么需要pipe花了我不少時間涕滋。使用bloc思維方式需要比較大的改變,我看到了許多人在項目中使用bloc挠阁,但是用得很奇怪宾肺,還在以之前的思維模式思考。而且bloc只是對數(shù)據(jù)進(jìn)行組織侵俗,共享狀態(tài)平時還是使用的InheritWidget锨用,確實要做很多額外的功夫。
其次我比較喜歡的就是scoped_model隘谣,理由就是簡單好用增拥。學(xué)習(xí)成本很低,而且沒有寫什么模版代碼。
我最不想使用的狀態(tài)管理方式就是redux了掌栅,一個是入門難度比較高秩仆,而且對于異步數(shù)據(jù)處理我也覺得是相當(dāng)麻煩的。但是閑魚團(tuán)隊倒是喜歡redux渣玲,之后還會開源閑魚的狀態(tài)管理框架fish_redux逗概。所以說,可能還是我編寫的應(yīng)用還不夠復(fù)雜忘衍,才會有這種感受逾苫。redux在復(fù)雜應(yīng)用上能夠更加清楚的劃分職責(zé),并且單向數(shù)據(jù)流以及state是immutable的特點這些都是redux的好處枚钓。
最后我再談?wù)凱rovide铅搓。Provide整體上給我的體驗非常接近Scoped,簡單易上手搀捷,并且更加強(qiáng)大星掰。model不用再繼承,只用實現(xiàn)Listenable讓它不再具有侵入性嫩舟。于此同時又增加了stream的特性氢烘,和bloc的做法又有幾分相似。如果你使用過Scoped_model你會很快就上手家厌。
不過可以說的是播玖,Provide是一個非常優(yōu)秀的狀態(tài)管理方式,值得你去使用饭于。但是目前該package還存在一些問題蜀踏,例如Provide.stream,在未來可能會進(jìn)行較大的變動掰吕,需要慎重使用果覆。
本次代碼已上傳Github: https://github.com/OpenFlutter/Flutter-Notebook/tree/master/mecury_project/example/flutter_provide
如果您對Provide還有任何疑問或者文章的建議,歡迎在下方評論區(qū)以及我的郵箱1652219550a@gmail.com與我聯(lián)系殖熟,我會及時回復(fù)局待!