概述
iOS 和 Android 的原生開發模式是指令式程式設計模式。指令式程式設計要求開發者一步步描述整個構建過程,從而載入程式去構建使用者介面。
Flutter 則採用了宣告式程式設計模式,框架隱藏了具體的構建過程,開發者只需要宣告狀態,框架會自動構建使用者介面。這也就意味著 Flutter 構建的使用者介面就是當前的狀態。
狀態管理
App 在執行中總是會更新使用者介面,因此我們需要對狀態進行有效的管理。狀態管理本質上就是 如何解決狀態讀/寫的問題。對此,我們將從兩個方面去評估狀態管理方案:
- 狀態訪問
- 狀態更新
此外,根據 Flutter 原生支援的情況,我們將 Flutter 狀態管理方案分為兩類:
- Flutter 內建的狀態管理方案
- 基於 Pub 的狀態管理方案
下文,我們將以 Flutter 官方的計數器例子來介紹 Flutter 中的狀態管理方案,並逐步進行優化。
關於本文涉及的原始碼,見【Demo 傳送門】。
Flutter 內建的狀態管理方案
直接訪問 + 直接更新
Flutter 模板工程就是【直接訪問 + 直接更新】的狀態管理方案。這種方案的狀態訪問/更新示意圖如下所示。
很顯然,【直接訪問 + 直接更新】方案只適合於在單個 StatefulWidget
中進行狀態管理。那麼對於多層級的 Widget 結構該如何進行狀態管理呢?
狀態傳遞 + 閉包傳遞
對於多層級的 Widget 結構,狀態是無法直接訪問和更新的。因為 Widget 和 State 是分離的,並且 State 一般都是私有的,所以子 Widget 是無法直接訪問/更新父 Widget 的 State。
對於這種情況,最直觀的狀態管理方案就是:【狀態傳遞 + 閉包傳遞】。對於狀態訪問,父 Widget 在建立子 Widget 時就將狀態傳遞給子 Widget;對於狀態更新,父 Widget 將更新狀態的操作封裝在閉包中,傳遞給子 Widget。
這裡存在一個問題:當 Widget 樹層級比較深時,如果中間有些 Widget 並不需要訪問或更新父 Widget 的狀態時,這些中間 Widget 仍然需要進行輔助傳遞。很顯然,這種方案在 Widget 樹層級較深時,效率比較低,只適合於較淺的 Widget 樹層級。
狀態傳遞 + Notification
那麼如何優化多層級 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);
}
複製程式碼
InheritedWidget + Notification
【傳遞傳遞 + 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 + EventBus
雖然【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 狀態管理方案。
基於 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 是一種常見的狀態管理方案。【Redux】方案的狀態管理示意圖與【Provider】方案基本上是一致的。
在這個基礎上,Redux 對於狀態更新的過程進行了進一步的細分和規劃,使得其資料的流動過程如下所示。
- 所有的狀態都儲存在 Store 中。一般會把 Store 放在 App 頂層。
- View 獲取 Store 中儲存的狀態。
- 當事件發生時,發出一個 action。
- Reducer 接收到 action,遍歷 action 表,找到匹配的 action,根據 action 生成新的狀態儲存到 Store 中。
- Store 儲存新狀態後,通知依賴該狀態的 view 更新。
一個 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
【BLoC】方案是谷歌的兩位工程師 Paolo Soares 和 Cong Hui 提出的一種狀態管理方案,其狀態管理示意圖同樣與【Provider】方案是一致的。
【BLoC】方案的底層實現與【Provider】是非常相似的,也是基於 InheritedWidget
進行狀態訪問,並且對狀態進行了封裝,從而提供直接更新狀態的方法。
但是,BLoC 的核心思想是 基於流來管理資料,並且將業務邏輯均放在 BLoC 中進行,從而實現檢視與業務的分離。
- BLoC 使用 Sink 作為輸入,使用 Stream 作為輸出。
- BLoC 內部會對輸入進行轉換,產生特定的輸出。
- 外部使用 StreamBuilder 監聽 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 模組中,實現業務分離。
總之,各種狀態管理方案都有著各自的優缺點,這些需要我們在實踐中去發現和總結,從而最終找到一種適合自己專案的狀態管理方案。