Flutter 狀態管理實踐

baochuquan發表於2020-06-06

原文連結

概述

iOS 和 Android 的原生開發模式是指令式程式設計模式。指令式程式設計要求開發者一步步描述整個構建過程,從而載入程式去構建使用者介面。

Flutter 則採用了宣告式程式設計模式,框架隱藏了具體的構建過程,開發者只需要宣告狀態,框架會自動構建使用者介面。這也就意味著 Flutter 構建的使用者介面就是當前的狀態。

Flutter 狀態管理實踐

狀態管理

App 在執行中總是會更新使用者介面,因此我們需要對狀態進行有效的管理。狀態管理本質上就是 如何解決狀態讀/寫的問題。對此,我們將從兩個方面去評估狀態管理方案:

  • 狀態訪問
  • 狀態更新

此外,根據 Flutter 原生支援的情況,我們將 Flutter 狀態管理方案分為兩類:

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

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

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

Flutter 內建的狀態管理方案

直接訪問 + 直接更新

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

Flutter 狀態管理實踐

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

狀態傳遞 + 閉包傳遞

對於多層級的 Widget 結構,狀態是無法直接訪問和更新的。因為 Widget 和 State 是分離的,並且 State 一般都是私有的,所以子 Widget 是無法直接訪問/更新父 Widget 的 State。

對於這種情況,最直觀的狀態管理方案就是:【狀態傳遞 + 閉包傳遞】。對於狀態訪問,父 Widget 在建立子 Widget 時就將狀態傳遞給子 Widget;對於狀態更新,父 Widget 將更新狀態的操作封裝在閉包中,傳遞給子 Widget。

這裡存在一個問題:當 Widget 樹層級比較深時,如果中間有些 Widget 並不需要訪問或更新父 Widget 的狀態時,這些中間 Widget 仍然需要進行輔助傳遞。很顯然,這種方案在 Widget 樹層級較深時,效率比較低,只適合於較淺的 Widget 樹層級。

Flutter 狀態管理實踐

狀態傳遞 + Notification

那麼如何優化多層級 Widget 樹結構下的狀態管理方案呢?我們首先從狀態更新方面進行優化。

【狀態傳遞 + Notification】方案採用 Notification 定向地優化了狀態更新的方式。

通知(Notification)是 Flutter 中一個重要的機制,在 Widget 樹種,每個節點都可以分發通知,通知會沿著當前節點向上傳遞,所有父節點都可以通過 NotificationListener 來監聽通知。Flutter 中將這種由子向父的傳遞通知的機制稱為 通知冒泡(Notification Bubbling)。通知冒泡和使用者觸控事件冒泡是相似的,但有一點不同:通知冒泡可以中止,而使用者觸控事件無法中止

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

Flutter 狀態管理實踐

具體的實現原始碼如下所示:

// 與 父 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 都能夠直接獲取到共享的資料。

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

Flutter 狀態管理實踐

具體的原始碼實現如下所示:

/// 與父 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 監聽到事件後進行狀態更新。

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

Flutter 狀態管理實踐

具體的原始碼實現如下所示:

/// 與父 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 獲取到狀態後,直接呼叫狀態的更新方法呢?

Flutter 狀態管理實踐

對此,官方推薦了一套基於第三方 Pub 的 Provider 狀態管理方案。

基於 Pub 的狀態管理方案

Provider

【Provider】的本質是 基於 InheritedWidgetChangeNotifier 進行了封裝。此外,使用快取提升了效能,避免不必要的重繪。

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

Flutter 狀態管理實踐

具體的原始碼實現如下所示:

/// 與父 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】方案基本上是一致的。

Flutter 狀態管理實踐

在這個基礎上,Redux 對於狀態更新的過程進行了進一步的細分和規劃,使得其資料的流動過程如下所示。

  • 所有的狀態都儲存在 Store 中。一般會把 Store 放在 App 頂層。
  • View 獲取 Store 中儲存的狀態。
  • 當事件發生時,發出一個 action。
  • Reducer 接收到 action,遍歷 action 表,找到匹配的 action,根據 action 生成新的狀態儲存到 Store 中。
  • Store 儲存新狀態後,通知依賴該狀態的 view 更新。

一個 Store 儲存多個狀態,適合用於全域性狀態管理。

Flutter 狀態管理實踐

具體的實現原始碼如下所示。

/// 與父 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】方案是一致的。

Flutter 狀態管理實踐

【BLoC】方案的底層實現與【Provider】是非常相似的,也是基於 InheritedWidget 進行狀態訪問,並且對狀態進行了封裝,從而提供直接更新狀態的方法。

但是,BLoC 的核心思想是 基於流來管理資料,並且將業務邏輯均放在 BLoC 中進行,從而實現檢視與業務的分離。

  • BLoC 使用 Sink 作為輸入,使用 Stream 作為輸出。
  • BLoC 內部會對輸入進行轉換,產生特定的輸出。
  • 外部使用 StreamBuilder 監聽 BLoC 的輸出(即狀態)。

Flutter 狀態管理實踐

具體的實現原始碼如下所示。

/// 與父 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) - 資料傳遞/狀態管理 | 掘金技術徵文

相關文章