Flutter 狀態管理實踐

原文連接git

概述

iOS 和 Android 的原生開發模式是命令式編程模式。命令式編程要求開發者一步步描述整個構建過程,從而引導程序去構建用戶界面。github

Flutter 則採用了聲明式編程模式,框架隱藏了具體的構建過程,開發者只須要聲明狀態,框架會自動構建用戶界面。這也就意味着 Flutter 構建的用戶界面就是當前的狀態。編程

狀態管理

App 在運行中老是會更新用戶界面,所以咱們須要對狀態進行有效的管理。狀態管理本質上就是 如何解決狀態讀/寫的問題。對此,咱們將從兩個方面去評估狀態管理方案:緩存

  • 狀態訪問
  • 狀態更新

此外,根據 Flutter 原生支持的狀況,咱們將 Flutter 狀態管理方案分爲兩類:markdown

  • Flutter 內置的狀態管理方案
  • 基於 Pub 的狀態管理方案

下文,咱們將以 Flutter 官方的計數器例子來介紹 Flutter 中的狀態管理方案,並逐步進行優化。閉包

關於本文涉及的源碼,見【Demo 傳送門】app

Flutter 內置的狀態管理方案

直接訪問 + 直接更新

Flutter 模板工程就是【直接訪問 + 直接更新】的狀態管理方案。這種方案的狀態訪問/更新示意圖以下所示。框架

很顯然,【直接訪問 + 直接更新】方案只適合於在單個 StatefulWidget 中進行狀態管理。那麼對於多層級的 Widget 結構該如何進行狀態管理呢?less

狀態傳遞 + 閉包傳遞

對於多層級的 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】的本質是 基於 InheritedWidgetChangeNotifier 進行了封裝。此外,使用緩存提高了性能,避免沒必要要的重繪。

下圖所示爲這種方案的狀態訪問/更新示意圖。

具體的源碼實現以下所示:

/// 與父 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 模塊中,實現業務分離。

總之,各類狀態管理方案都有着各自的優缺點,這些須要咱們在實踐中去發現和總結,從而最終找到一種適合本身項目的狀態管理方案。

參考

  1. 狀態 (State) 管理參考
  2. [譯]讓我來幫你理解和選擇Flutter狀態管理方案
  3. Flutter狀態管理 - 初探與總結
  4. Flutter | 狀態管理探索篇——Scoped Model(一)
  5. Flutter | 狀態管理探索篇——Redux(二)
  6. Flutter | 狀態管理探索篇——BLoC(三)
  7. 《Flutter 實戰》
  8. Dart | 什麼是Stream
  9. 異步編程:使用 stream
  10. 使用 Flutter 構建響應式移動應用
  11. Flutter入門三部曲(3) - 數據傳遞/狀態管理 | 掘金技術徵文
相關文章
相關標籤/搜索