在響應(yīng)式編程中塘秦,狀態(tài)即數(shù)據(jù)建蹄,狀態(tài)變化碌更,頁面即發(fā)生變化裕偿,F(xiàn)lutter作為響應(yīng)式開發(fā)框架,狀態(tài)管理是Flutter開發(fā)過程中代碼架構(gòu)的重點(diǎn)痛单,本文中嘿棘,我們將通過分析常用的Flutter狀態(tài)管理框架,給大家深入解析狀態(tài)管理的核心實(shí)現(xiàn)方法旭绒,方便大家在后續(xù)開發(fā)中鸟妙,挑選合適的狀態(tài)管理框架。
Flutter本身已經(jīng)給我們提供了一個(gè)狀態(tài)管理方式快压,即Flutter自帶的StatefulWidget圆仔,但是我們?cè)趹?yīng)用過程中垃瞧,會(huì)發(fā)現(xiàn)蔫劣,這個(gè)狀態(tài)僅僅適合在單個(gè)StatefulWidget中進(jìn)行維護(hù),當(dāng)我們需要一個(gè)跨組件狀態(tài)時(shí)个从,StatefulWidget將不再是一個(gè)好的選擇脉幢,雖然我們可以使用callBack進(jìn)行解決,但這個(gè)方式一旦業(yè)務(wù)增長到一定程度嗦锐,嵌套較深的時(shí)候嫌松,將會(huì)造成很大的代碼耦合,因此奕污,我們需要使用一個(gè)狀態(tài)管理組件進(jìn)行維護(hù)這些狀態(tài)萎羔。
常用的狀態(tài)管理組件,包含了ScopedModel碳默,BLoC贾陷,RxDart,Provider等嘱根,今天我們將在這里對(duì)這些狀態(tài)管理機(jī)制的使用方法及原理進(jìn)行剖析髓废。
一、 狀態(tài)管理分類:
首先该抒,常用的狀態(tài)管理慌洪,按照范圍可以劃分為局部狀態(tài)管理和全局狀態(tài)管理:
局部狀態(tài):
Flutter提供了類似StatefulWidget、InheritWidget組件來實(shí)現(xiàn)局部狀態(tài)管理凑保,當(dāng)這些Widget發(fā)生變化時(shí)冈爹,所有子樹中依賴其數(shù)據(jù)的widget都會(huì)進(jìn)行rebuild。
全局狀態(tài):
Flutter沒有提供原生的全局狀態(tài)管理機(jī)制欧引,雖然可以在根布局控件使用InheritWidget來實(shí)現(xiàn)全局狀態(tài)管理频伤,但是這樣會(huì)存在類似依賴傳遞過深等問題。因此大多數(shù)情況下维咸,需要依賴一些第三方庫實(shí)現(xiàn)全局狀態(tài)管理
最簡單的狀態(tài)管理
我們可以使用 State + InheritedWidget實(shí)現(xiàn)最簡單的狀態(tài)管理機(jī)制剂买。
二惠爽、 狀態(tài)管理——Stream
Stream在Flutter中標(biāo)志著的事件流或者管道一類的概念,通過Stream可以快速的實(shí)現(xiàn)給予事件流驅(qū)動(dòng)的業(yè)務(wù)邏輯瞬哼。界面通過訂閱事件婚肆,并針對(duì)各個(gè)事件進(jìn)行變化處理,實(shí)現(xiàn)響應(yīng)式更新界面坐慰。
/// 展示文本
String textString = "等待接收文本";
/// 單訂閱Stream
Stream<String> stream = new Stream.fromFuture(_doFutureTask());
@override
void initState() {
StreamSubscription subscription = stream.listen((data) {
textString = data;
print("接收數(shù)據(jù)成功");
}, onDone: () {
print("流處理完成");
}, onError: () {
print("流處理出現(xiàn)異常");
});
/// Subscription_API
subscription.cancel();
subscription.pause();
subscription.resume();
}
/// 異步任務(wù)
static _doFutureTask() {
return Future.delayed(Duration(seconds: 5), () {
print('Hello World');
});
}
這是一個(gè)最簡單的较性,通過異步任務(wù)創(chuàng)建流對(duì)象的方法,包含了流的創(chuàng)建结胀、監(jiān)聽赞咙、管理,Stream流糟港,分為單訂閱流和廣播流
單訂閱流
單訂閱流只允許在該Stream的整個(gè)生命周期攀操,注冊(cè)一個(gè)監(jiān)聽器,即使第一個(gè)監(jiān)聽器被取消了秸抚,也無法在這個(gè)時(shí)間流中速和,監(jiān)聽到第二次事件。
// 初始化
StreamController<String> singleStream = StreamController();
// 消息發(fā)送
singleStream.add('Hello World');
使用過程中剥汤,經(jīng)常會(huì)在log平臺(tái)輸出: Bad state: Stream has already been listened to.
其含義就是指:單訂閱流不能有多個(gè)收聽者
廣播流
廣播流颠放,和單訂閱流不同,允許任意個(gè)數(shù)的監(jiān)聽者吭敢,可以隨時(shí)隨地為其添加監(jiān)聽器碰凶,只要新的監(jiān)聽器,被添加進(jìn)去鹿驼,就可以收到新的事件
// 初始化
StreamController<int> singleStream = StreamController.broadcast();
// 消息發(fā)送
singleStream.sink.add(6);
StreamController的構(gòu)造函數(shù)是一個(gè)泛型欲低,意味著StreamController可以往流上推送任意類型的數(shù)據(jù),當(dāng)然這里需要考慮接收時(shí)候的數(shù)據(jù)類型處理蠢沿。
StreamBuilder如何實(shí)現(xiàn)刷新
在上邊的例子中伸头,我們看到了在頁面中有使用一個(gè)StreamBuilder,來構(gòu)建一個(gè)UI展示:
StreamBuilder<String>(
builder: (context, snapshot) {
if (snapshot == null || !snapshot.hasData) {
return CircularProgressIndicator();
} else {
if (snapshot.hasError) {
return Text("發(fā)生錯(cuò)誤");
} else {
return Text(snapshot.data);
}
}
},
stream: singleStream.stream,
)
那么StreamBuilder和Widget的刷新舷蟀,是怎么關(guān)聯(lián)起來的呢恤磷?通過閱讀代碼,我們發(fā)現(xiàn)野宜,其實(shí)StreamBuilder的主要邏輯在_StreamBuilderBaseState中扫步,_StreamBuilderBaseState在initState、didUpdateWidget中會(huì)調(diào)用_subscribe方法匈子,從而調(diào)用Stream的listen河胎,然后通過setState更新UI。
void _subscribe() {
if (widget.stream != null) {
_subscription = widget.stream.listen((T data) {
setState(() {
_summary = widget.afterData(_summary, data);
});
}, onError: (Object error) {
setState(() {
_summary = widget.afterError(_summary, error);
});
}, onDone: () {
setState(() {
_summary = widget.afterDone(_summary);
});
});
_summary = widget.afterConnected(_summary);
}
}
而setState的刷新機(jī)制虎敦,其實(shí)我們大家應(yīng)該都知道游岳,實(shí)質(zhì)上是調(diào)用了markNeedsBuild政敢,markNeedsBuild方法會(huì)標(biāo)記element為dirty,這樣在下一幀WidgetsBinding.drawFrame的時(shí)候胚迫,會(huì)進(jìn)行繪制
StreamController整體架構(gòu)
從上邊幾個(gè)demo中喷户,我們看到Flutter的Stream流中,存在Stream访锻、StreamController褪尝、Sink、以及StreamSubscription這樣四個(gè)比較關(guān)鍵的組件期犬,那么這四個(gè)組件是以一個(gè)什么樣子的形式互相結(jié)合起來的呢河哑。我們通過下邊這樣一副圖,進(jìn)行說明:
整個(gè)流程龟虎,概括起來就是:StreamController作為一個(gè)統(tǒng)籌管理的“Boss”璃谨,主要負(fù)責(zé)協(xié)調(diào)和維護(hù)整個(gè)事件流的輸入和輸出,StreamController暴露了一個(gè)Sink屬性遣总,主要負(fù)責(zé)事件流的輸入睬罗,在這里輸入事件轨功。暴露一個(gè)Stream屬性旭斥,主要負(fù)責(zé)流事件的輸出,除自身提供了事件轉(zhuǎn)換方法古涧,例如where垂券、take等,主要進(jìn)行事件流的轉(zhuǎn)換羡滑。同時(shí)菇爪,Stream對(duì)外提供了事件的監(jiān)聽,分別可以處理在收到事件以后的處理onData柒昏,事件處理完成以后的onDone以及事件處理異常的onError等方法凳宙,通過注冊(cè)這樣的監(jiān)聽,我們又可以得到StreamSubscription這個(gè)屬性职祷,其功能氏涩,主要管理事件的訂閱,包含取消有梆、暫停是尖、恢復(fù)等操作
StreamController同步、異步處理
在初始化StreamController的時(shí)候泥耀,我們可以看到饺汹,有一個(gè)構(gòu)造參數(shù),sync痰催,針對(duì)我們傳遞的sync值兜辞,決定使用同步流還是異步流迎瞧。
return sync
? new _SyncStreamController<T>(onListen, onPause, onResume, onCancel)
: new _AsyncStreamController<T>(onListen, onPause, onResume, onCancel);
這里具體怎么實(shí)現(xiàn)的呢,我們一起來看一下逸吵。在同步流中夹攒,直接調(diào)用了subscription的_add方法,直接將數(shù)據(jù)添加進(jìn)事件回掉監(jiān)聽中胁塞,實(shí)現(xiàn)同步:
void _sendData(T data) {
if (_isEmpty) return;
if (_hasOneListener) {
_state |= _BroadcastStreamController._STATE_FIRING;
_BroadcastSubscription<T> subscription = _firstSubscription;
subscription._add(data);
_state &= ~_BroadcastStreamController._STATE_FIRING;
if (_isEmpty) {
_callOnCancel();
}
return;
}
_forEachListener((_BufferingStreamSubscription<T> subscription) {
subscription._add(data);
});
}
而在異步流中咏尝,則是使用_addPending方法,添加了一個(gè)繼承自_DelayedEvent的方法啸罢,實(shí)現(xiàn)異步:
abstract class _AsyncStreamControllerDispatch<T>
implements _StreamController<T> {
void _sendData(T data) {
_subscription._addPending(new _DelayedData<T>(data));
}
void _sendError(Object error, StackTrace stackTrace) {
_subscription._addPending(new _DelayedError(error, stackTrace));
}
void _sendDone() {
_subscription._addPending(const _DelayedDone());
}
}
至此编检,我們已經(jīng)基本了解了Stream的原理及一些常用的api,這方便我們對(duì)后續(xù)的RxDart扰才、Provider等進(jìn)行分析
三允懂、狀態(tài)管理——RxDart
說到RxDart就不得不提一下ReactiveX,http://reactivex.io/
在其官網(wǎng)上衩匣,對(duì)ReactiveX的介紹為
“An API for asynchronous programming with observable streams”
用于可觀察流的異步編程的API蕾总,一句話概括了ReactiveX的核心設(shè)計(jì)思想,為各個(gè)平臺(tái)提供了異步編程的可觀察流API。當(dāng)然在Dart上也不例外愤兵。
/// 創(chuàng)建一個(gè)Subject
var subject = PublishSubject<String>();
/// 通過listen實(shí)現(xiàn)訂閱
subject.listen((String data) {
print("OnData " + data);
}, onError: () {
print("onError ");
}, onDone: () {
print("onDone ");
});
/// 使用完成飞蹂,關(guān)閉
subject.close();
這里是一個(gè)最簡單的RxDart的使用方法,細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)了蚀浆,這不就是之前的StreamController么。的確是這樣的搜吧,RxDart內(nèi)部市俊,其實(shí)也是對(duì)Stream的一個(gè)封裝。PublishSubject的內(nèi)部實(shí)現(xiàn)滤奈,其實(shí)也是一個(gè)廣播類型的StreamController:
factory PublishSubject({void onListen(), void onCancel(), bool sync = false}) {
// ignore: close_sinks
final controller = StreamController<T>.broadcast(
onListen: onListen,
onCancel: onCancel,
sync: sync,
);
return PublishSubject<T>._(
controller,
Observable<T>(controller.stream),
);
}
至此我們也明白了:
在RxDart中Subject摆昧,無論是PublishSubject還是BehaviorSubject(只保留最后一個(gè)值的特殊流),其核心其實(shí)還是StreamController
RxDart中的Observable實(shí)質(zhì)上相當(dāng)于一個(gè)Stream
總結(jié)一下蜒程,RxDart绅你,實(shí)際是對(duì)上邊所說的Stream進(jìn)行了概念轉(zhuǎn)換,變成了ReactiveX用戶熟悉的對(duì)象和操作符搞糕,本質(zhì)上還是一個(gè)Stream勇吊,這也是為什么可以直接在StreamBuilder中使用RxDart。
四窍仰、狀態(tài)管理——Provider
Provider是Flutter官方推薦的狀態(tài)管理方式之一汉规,它的特點(diǎn)是,不復(fù)雜,好理解针史,可控度較高晶伦。
Provider使用
第一步 添加依賴
provider: ^2.0.1+1
第二步 創(chuàng)建數(shù)據(jù)模型
/// 計(jì)數(shù)module
class CounterModel extends ChangeNotifier {
/// 計(jì)數(shù)
int _count = 0;
int get value => _count;
void increment() {
_count++;
notifyListeners();
}
}
該Module繼承自ChangeNotifier,主要負(fù)責(zé)數(shù)據(jù)模型保存和管理啄枕,同時(shí)暴露出來的notifyListeners()方法婚陪,在調(diào)用后,可以自動(dòng)更新其所有的監(jiān)聽者频祝。
第三步 創(chuàng)建全局共享數(shù)據(jù)依賴
void main() {
/// 數(shù)據(jù)模型
final counter = CounterModel();
runApp(
ChangeNotifierProvider.value(
notifier: counter,
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: FirstScreen(),
);
}
}
在這里我們使用ChangeNotifierProvider泌参,并設(shè)置其value為之前創(chuàng)建的數(shù)據(jù)模型。ChangeNotifierProvider<T>.value類型的數(shù)據(jù)常空,不僅可以將數(shù)據(jù)共享給其所有的子節(jié)點(diǎn)進(jìn)行使用沽一,同時(shí)還可以在數(shù)據(jù)發(fā)生變化時(shí),通過調(diào)用之前數(shù)據(jù)模型中的notifyListeners()方法進(jìn)行刷新漓糙。
第四步 在其他頁面中獲取共享的狀態(tài)
這里我們通過兩個(gè)頁面铣缠,相互之間共同持有一份數(shù)據(jù)的例子,來看看Provider是怎么處理兩個(gè)頁面之間數(shù)據(jù)的共享的昆禽,首先我們創(chuàng)建第一個(gè)頁面
/// Provider 頁面
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("Provider " + "FirstScreen build");
final _counter = Provider.of<CounterModel>(context);
return Scaffold(
appBar: AppBar(title: Text('FirstPage')),
body: Center(
child: GestureDetector(
onTap: () => {_counter.increment()},
child: Text(
'Value: ${_counter.value}',
style: TextStyle(fontSize: 48),
))),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => SecondPage())),
child: Icon(Icons.navigate_next),
),
);
}
}
在這個(gè)頁面中蝗蛙,我們使用Provider.of<T>(context)方法來向上尋找最近存儲(chǔ)了T的祖先節(jié)點(diǎn)數(shù)據(jù)。我們這里獲取到了存儲(chǔ)的CounterModel醉鳖,并對(duì)其屬性value進(jìn)行展示捡硅。然后在點(diǎn)擊文本的時(shí)候,自增辐棒,希望在第二個(gè)頁面中病曾,可以拿到并展示。
/// Provider 頁面
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext buildContext) {
print("Provider " + "SecondPage build");
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Consumer<CounterModel>(
builder: (context, CounterModel counter, _) => Center(
child: GestureDetector(
onTap: () => {counter.increment()},
child: Text(
'Value: ${counter.value}',
style: TextStyle(
fontSize: 48,
),
))),
));
}
}
在第二個(gè)頁面中漾根,我們并沒有像在第一個(gè)頁面中一樣,使用Provider.of<T>(context)方式去獲取Provider中共享的數(shù)據(jù)鲫竞,而是使用了Consumer這個(gè)方式去獲取辐怕,這兩個(gè)有什么不一樣呢,先看完演示結(jié)果从绘,然后繼續(xù)往下分析寄疏。
Consumer
在上邊的例子中,我們使用了Consumer獲取Provider中共享的數(shù)據(jù)模型僵井,Consumer使用了Builder模式陕截,收到更新通知就會(huì)通過builder重新構(gòu)建。Consumer<T>代表了它要獲取哪一個(gè)祖先中的Model批什。
final Widget Function(BuildContext context, T value, Widget child) builder;
從Consumer的構(gòu)造方法中农曲,我們可以看到,其builder實(shí)際上就是一個(gè)Funcation,它接受三個(gè)參數(shù)乳规,用于構(gòu)建自身形葬。同樣原理的還有Consumer2,和Consumer類似暮的,只是入?yún)⒌姆盒腕弦裕兂闪藘蓚€(gè):
final Widget Function(BuildContext context, A value, B value2, Widget child) builder;
其實(shí)在源碼中,這里最多可以到6個(gè)冻辩,大家在使用的過程中猖腕,可以根據(jù)自己需要進(jìn)行選取。
那么為什么需要區(qū)分這兩個(gè)獲取數(shù)據(jù)類型的方法呢恨闪,我們?cè)谏鲜鰞蓚€(gè)頁面的build方法中谈息,分別添加了log,進(jìn)行日志打印凛剥,操作步驟為:
1. 進(jìn)入第一個(gè)頁面侠仇,點(diǎn)擊兩次Value,使數(shù)據(jù)自增
2. 點(diǎn)擊進(jìn)入下一個(gè)頁面
3. 在第二個(gè)頁面犁珠,同樣點(diǎn)擊兩次Value逻炊,使數(shù)據(jù)自增
4. 返回第一個(gè)頁面,點(diǎn)擊一次Value犁享,自增
我們可以看到Log打印如下:
2020-12-12 18:30:26.873 4273-6507/jd.com.state.flutter_state_manager I/flutter: Provider FirstScreen build
2020-12-12 18:30:27.777 4273-6507/jd.com.state.flutter_state_manager I/flutter: Provider FirstScreen build
2020-12-12 18:30:29.290 4273-6507/jd.com.state.flutter_state_manager I/flutter: Provider SecondPage build
2020-12-12 18:30:30.203 4273-6507/jd.com.state.flutter_state_manager I/flutter: Provider FirstScreen build
2020-12-12 18:30:31.726 4273-6507/jd.com.state.flutter_state_manager I/flutter: Provider FirstScreen build
2020-12-12 18:30:35.223 4273-6507/jd.com.state.flutter_state_manager I/flutter: Provider FirstScreen build
可以看到使用Provider.of<T>(context)的第一個(gè)頁面余素,在每一次點(diǎn)擊按鈕的時(shí)候,都會(huì)重新build炊昆,而頁面二桨吊,則沒有。也就是說凤巨,使用Consumer進(jìn)行數(shù)據(jù)共享的時(shí)候视乐,僅僅只更新自身的Widget。那么為什么Consumer可以做到局部更新呢敢茁,我們來看一下Consumer的內(nèi)部構(gòu)造:
@override
Widget build(BuildContext context) {
return builder(
context,
Provider.of<T>(context),
child,
);
}
也就是在Consumer中佑淀,實(shí)際上,也是通過使用Provider.of<T>(context),來實(shí)現(xiàn)的彰檬。那么問題就來了伸刃,同樣是Provider.of<T>(context)實(shí)現(xiàn)的,為什么Consumer就可以實(shí)現(xiàn)局部刷新呢逢倍?
這里我們可以看一下在第二個(gè)頁面中捧颅,我們?cè)跇?gòu)建Consumer的時(shí)候,傳遞的context较雕,并不是Widget中build方法提供的buildContext碉哑,而是使用了自己的context,所以在刷新的時(shí)候,可以做到局部刷新谭梗,這樣就方便我們?cè)贔lutter開發(fā)的時(shí)候忘晤,使用局部刷新進(jìn)行頁面性能優(yōu)化。
至此激捏,我們已經(jīng)基本上了解到了Provider的基礎(chǔ)用法设塔,當(dāng)然Provider還提供了核心的dispose方法,方便用戶進(jìn)行回收远舅,這里因?yàn)檫€沒有徹底搞清楚闰蛔,所以暫不進(jìn)行介紹。
Provider還有更多图柏,更詳盡的用法序六,比如 ValueListenableProvider、FutureProvider蚤吹、StreamProvider等多種Provider例诀,可見整個(gè)Provider的設(shè)計(jì)上更貼近Flutter的原生特性,同時(shí)設(shè)計(jì)也更好理解裁着,并且兼顧了性能等問題繁涂。這些后邊在使用到的時(shí)候,我們?cè)谶M(jìn)行逐步分析和解析
五二驰、 總結(jié)
總結(jié)上述所有的狀態(tài)管理機(jī)制扔罪,無論是Flutter原生提供的Stream,還是ReactiveX提供的RxDart桶雀,亦或是Provider矿酵,以及沒有在文章中出現(xiàn)的scoped_model、阿里開源的fish_redux矗积,這一系列的組件全肮,都為我們提供了一個(gè)很好的狀態(tài)管理機(jī)制,而我們?cè)谑褂眠^程中漠魏,大可通過自身業(yè)務(wù)需求倔矾,按需選型。