原文連接git
iOS 和 Android 的原生開發模式是命令式編程模式。命令式編程要求開發者一步步描述整個構建過程,從而引導程序去構建用戶界面。github
Flutter 則採用了聲明式編程模式,框架隱藏了具體的構建過程,開發者只須要聲明狀態,框架會自動構建用戶界面。這也就意味着 Flutter 構建的用戶界面就是當前的狀態。編程
App 在運行中老是會更新用戶界面,所以咱們須要對狀態進行有效的管理。狀態管理本質上就是 如何解決狀態讀/寫的問題。對此,咱們將從兩個方面去評估狀態管理方案:緩存
此外,根據 Flutter 原生支持的狀況,咱們將 Flutter 狀態管理方案分爲兩類:markdown
下文,咱們將以 Flutter 官方的計數器例子來介紹 Flutter 中的狀態管理方案,並逐步進行優化。閉包
關於本文涉及的源碼,見【Demo 傳送門】。app
Flutter 模板工程就是【直接訪問 + 直接更新】的狀態管理方案。這種方案的狀態訪問/更新示意圖以下所示。框架
很顯然,【直接訪問 + 直接更新】方案只適合於在單個 StatefulWidget
中進行狀態管理。那麼對於多層級的 Widget 結構該如何進行狀態管理呢?less
對於多層級的 Widget 結構,狀態是沒法直接訪問和更新的。由於 Widget 和 State 是分離的,而且 State 通常都是私有的,因此子 Widget 是沒法直接訪問/更新父 Widget 的 State。異步
對於這種狀況,最直觀的狀態管理方案就是:【狀態傳遞 + 閉包傳遞】。對於狀態訪問,父 Widget 在建立子 Widget 時就將狀態傳遞給子 Widget;對於狀態更新,父 Widget 將更新狀態的操做封裝在閉包中,傳遞給子 Widget。
這裏存在一個問題:當 Widget 樹層級比較深時,若是中間有些 Widget 並不須要訪問或更新父 Widget 的狀態時,這些中間 Widget 仍然須要進行輔助傳遞。很顯然,這種方案在 Widget 樹層級較深時,效率比較低,只適合於較淺的 Widget 樹層級。
那麼如何優化多層級 Widget 樹結構下的狀態管理方案呢?咱們首先從狀態更新方面進行優化。
【狀態傳遞 + Notification】方案採用 Notification 定向地優化了狀態更新的方式。
通知(Notification)是 Flutter 中一個重要的機制,在 Widget 樹種,每一個節點均可以分發通知,通知會沿着當前節點向上傳遞,全部父節點均可以經過 NotificationListener
來監聽通知。Flutter 中將這種由子向父的傳遞通知的機制稱爲 通知冒泡(Notification Bubbling)。通知冒泡和用戶觸摸事件冒泡是類似的,但有一點不一樣:通知冒泡能夠停止,而用戶觸摸事件沒法停止。
下圖所示爲這種方案的狀態訪問/更新示意圖。
具體的實現源碼以下所示:
// 與 父 Widget 綁定的 State
class _PassStateNotificationDemoPageState extends State<PassStateNotificationDemoPage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
// 父 Widget 使用 NotificationListener 監聽通知
return NotificationListener<IncrementNotification>(
onNotification: (notification) {
setState(() {
_incrementCounter();
});
return true; // true: 阻止冒泡;false: 繼續冒泡
},
child: Scaffold(
...
),
);
}
}
/// 子 Widget
class _IncrementButton extends StatelessWidget {
int counter = 0;
_IncrementButton(this.counter);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => IncrementNotification("加一操做").dispatch(context), // 點擊按鈕觸發通知派發
child: ...)
);
}
}
/// 自定義通知
class IncrementNotification extends Notification {
final String msg;
IncrementNotification(this.msg);
}
複製代碼
【傳遞傳遞 + Notification】方案定向優化了狀態的更新,那麼如何進一步優化狀態的訪問呢?
【InheritedWidget + Notification】方案採用 InhertiedWidget
實現了在多層級 Widget 樹中直接訪問狀態的能力。
InheritedWidget
是 Flutter 中很是重要的一個功能型組件,其提供了一種數據在 Widget 樹中從上到下傳遞、共享的方式。這與 Notification 的傳遞方向正好相反。咱們在父 Widget 中經過 InheritedWidget
共享一個數據,那麼任意子 Widget 都可以直接獲取到共享的數據。
下圖所示爲這種方案的狀態訪問/更新示意圖。
具體的源碼實現以下所示:
/// 與父 Widget 綁定的 State
class _InheritedWidgetNotificationDemoPageState extends State<InheritedWidgetNotificationDemoPage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return CounterInheritedWidget(
counter: _counter,
child: NotificationListener<IncrementNotification>(
onNotification: (notification) {
setState(() {
_incrementCounter();
});
return true; // true: 阻止冒泡;false: 繼續冒泡
},
child: Scaffold(
...
),
),
),
),
);
}
}
/// 子 Widget
class _IncrementButton extends StatelessWidget {
_IncrementButton();
@override
Widget build(BuildContext context) {
// 直接獲取狀態
final counter = CounterInheritedWidget.of(context).counter;
return GestureDetector(
onTap: () => IncrementNotification("加一").dispatch(context), // 派發通知
child: ...
);
}
}
/// 對使用自定義的 InheritedWidget 子類對狀態進行封裝
class CounterInheritedWidget extends InheritedWidget {
final int counter;
// 須要在子樹中共享的數據,保存點擊次數
CounterInheritedWidget({@required this.counter, Widget child}) : super(child: child);
// 定義一個便捷方法,方便子樹中的widget獲取共享數據
static CounterInheritedWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();
}
@override
bool updateShouldNotify(CounterInheritedWidget old) {
// 若是返回true,則子樹中依賴(build函數中有調用)本widget
// 的子widget的`state.didChangeDependencies`會被調用
return old.counter != counter;
}
}
複製代碼
雖然【InheritedWidget + Notification】方案在狀態訪問和狀態更新方面都進行了優化,可是從其狀態管理示意圖上看,狀態的更新仍然具備優化空間。
【InheritedWidget + EventBus】方案則採用了 事件總線(Event Bus)的方式管理狀態更新。
事件總線是 Flutter 中的一種全局廣播機制,能夠實現跨頁面事件通知。事件總線一般是一種訂閱者模式,其包含發佈者和訂閱者兩種角色。
【InheritedWidget + EventBus】方案將子 Widget 做爲發佈者,父 Widget 做爲訂閱者。當子 Widget 進行狀態更新時,則發出事件,父 Widget 監聽到事件後進行狀態更新。
下圖所示爲這種方案的狀態訪問/更新示意圖。
具體的源碼實現以下所示:
/// 與父 Widget 綁定的狀態
class _InheritedWidgetEventBusDemoPageState extends State<InheritedWidgetEventBusDemoPage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
void initState() {
super.initState();
// 訂閱事件
bus.on(EventBus.incrementEvent, (_) {
_incrementCounter();
});
}
@override
void dispose() {
// 取消訂閱
bus.off(EventBus.incrementEvent);
super.dispose();
}
...
}
/// 子 Widget
class _IncrementButton extends StatelessWidget {
_IncrementButton();
@override
Widget build(BuildContext context) {
final counter = CounterInheritedWidget.of(context).counter;
return GestureDetector(
onTap: () => bus.emit(EventBus.incrementEvent), // 發佈事件
child: ...
);
}
}
複製代碼
【InheritedWidget + Notification】和【InheritedWidget + EventBus】的區別主要在於狀態更新。二者對於狀態的更新其實並無達到最佳狀態,都是經過一種間接的方式實現的。
相比而言,事件總線是基於全局,邏輯難以進行收斂,而且還要管理監聽事件、取消訂閱。從這方面而言,【InheritedWidget + Notification】方案更優。
從狀態管理示意圖而言,顯然【InheritedWidget + Notification】還有進一步的優化空間。這裏,咱們可能會想:狀態可否直接提供更新方法,當子 Widget 獲取到狀態後,直接調用狀態的更新方法呢?
對此,官方推薦了一套基於第三方 Pub 的 Provider 狀態管理方案。
【Provider】的本質是 基於 InheritedWidget
和 ChangeNotifier
進行了封裝。此外,使用緩存提高了性能,避免沒必要要的重繪。
下圖所示爲這種方案的狀態訪問/更新示意圖。
具體的源碼實現以下所示:
/// 與父 Widget 綁定的 State
class _ProviderDemoPageState extends State<ProviderDemoPage> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<CounterProviderState>(
create: (_) => CounterProviderState(), // 建立狀態
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
// 使用 provider 提供的 builder 使用狀態
Consumer<CounterProviderState>(builder: (context, counter, _) => Text("${counter.value}", style: Theme.of(context).textTheme.display1)),
_IncrementButton(),
],
),
),
),
);
}
}
/// 子 Widget
class _IncrementButton extends StatelessWidget {
_IncrementButton();
@override
Widget build(BuildContext context) {
// 訪問狀態
final _counter = Provider.of<CounterProviderState>(context);
return GestureDetector(
onTap: () => _counter.incrementCounter(), // 更新狀態
child: ...
);
}
}
/// 自定義的狀態,繼承自 ChangeNotifier
class CounterProviderState with ChangeNotifier {
int _counter = 0;
int get value => _counter;
// 狀態提供的更新方法
void incrementCounter() {
_counter++;
notifyListeners();
}
}
複製代碼
Flutter 社區早期使用的 Scoped Model 方案與 Provider 的實現原理基本是一致的。
對於聲明式(響應式)編程中的狀態管理,Redux 是一種常見的狀態管理方案。【Redux】方案的狀態管理示意圖與【Provider】方案基本上是一致的。
在這個基礎上,Redux 對於狀態更新的過程進行了進一步的細分和規劃,使得其數據的流動過程以下所示。
一個 Store 存儲多個狀態,適合用於全局狀態管理。
具體的實現源碼以下所示。
/// 與父 Widget 綁定的 State
class _ReduxDemoPageState extends State<ReduxDemoPage> {
// 初始化 Store,該過程包括了對 State 的初始化
final store = Store<CounterReduxState>(reducer, initialState: CounterReduxState.initState());
@override
Widget build(BuildContext context) {
return StoreProvider<CounterReduxState>(
store: store,
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
// 經過 StoreConnector 訪問狀態
StoreConnector<CounterReduxState, int>(
converter: (store) => store.state.value,
builder: (context, count) {
return Text("$count", style: Theme.of(context).textTheme.display1);
},
),
_IncrementButton(),
],
),
),
),
);
}
}
/// 子 Widget
class _IncrementButton extends StatelessWidget {
_IncrementButton();
@override
Widget build(BuildContext context) {
return StoreConnector<CounterReduxState, VoidCallback>(
converter: (store) {
return () => store.dispatch(Action.increment); // 發出 Action 以進行狀態更新
},
builder: (context, callback) {
return GestureDetector(
onTap: callback,
child: StoreConnector<CounterReduxState, int>(
converter: (store) => store.state.value,
builder: (context, count) {
return ...;
},
)
);
},
);
}
}
/// 自定義狀態
class CounterReduxState {
int _counter = 0;
int get value => _counter;
CounterReduxState(this._counter);
CounterReduxState.initState() {
_counter = 0;
}
}
/// 自定義 Action
enum Action{
increment
}
/// 自定義 Reducer
CounterReduxState reducer(CounterReduxState state, dynamic action) {
if (action == Action.increment) {
return CounterReduxState(state.value + 1);
}
return state;
}
複製代碼
【BLoC】方案是谷歌的兩位工程師 Paolo Soares 和 Cong Hui 提出的一種狀態管理方案,其狀態管理示意圖一樣與【Provider】方案是一致的。
【BLoC】方案的底層實現與【Provider】是很是類似的,也是基於 InheritedWidget
進行狀態訪問,而且對狀態進行了封裝,從而提供直接更新狀態的方法。
可是,BLoC 的核心思想是 基於流來管理數據,而且將業務邏輯均放在 BLoC 中進行,從而實現視圖與業務的分離。
具體的實現源碼以下所示。
/// 與父 Widget 綁定的 State
class _BlocDemoPageState extends State<BlocDemoPage> {
// 建立狀態
final bloc = CounterBloc();
@override
Widget build(BuildContext context) {
// 以 InheritedWidget 的方式提供直接方案
return BlocProvider(
bloc: bloc,
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
// 狀態訪問
StreamBuilder<int>(stream: bloc.value, initialData: 0, builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return Text("${snapshot.data}", style: Theme.of(context).textTheme.display1);
},),
_IncrementButton(),
],
),
),
)
);
}
}
/// 子 Widget
class _IncrementButton extends StatelessWidget {
_IncrementButton();
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => BlocProvider.of(context).increment(), // 狀態更新
child: ClipOval(child: Container(width: 50, height: 50, alignment: Alignment.center,color: Colors.blue, child: StreamBuilder<int>(stream: BlocProvider.of(context).value, initialData: 0, builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
// 狀態訪問
return Text("${snapshot.data}", textAlign: TextAlign.center,style: TextStyle(fontSize: 24, color: Colors.white));
},),),)
);
}
}
/// 自定義 BLoC Provider,繼承自 InheritedWidget
class BlocProvider extends InheritedWidget {
final CounterBloc bloc;
BlocProvider({this.bloc, Key key, Widget child}) : super(key: key, child: child);
@override
bool updateShouldNotify(_) => true;
static CounterBloc of(BuildContext context) => (context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bloc;
}
/// 自定義的狀態
class CounterBloc {
int _counter;
StreamController<int> _counterController;
CounterBloc() {
_counter = 0;
_counterController = StreamController<int>.broadcast();
}
Stream<int> get value => _counterController.stream;
increment() {
_counterController.sink.add(++_counter);
}
dispose() {
_counterController.close();
}
}
複製代碼
通常而言,對於普通的項目來講【Provider】方案是一種很是容易理解,而且實用的狀態管理方案。
對於大型的項目而言,【Redux】 有一套相對規範的狀態更新流程,可是模板代碼會比較多;對於重業務的項目而言,【BLoC】可以將複雜的業務內聚到 BLoC 模塊中,實現業務分離。
總之,各類狀態管理方案都有着各自的優缺點,這些須要咱們在實踐中去發現和總結,從而最終找到一種適合本身項目的狀態管理方案。