19

Flutter 状态管理实践

 4 years ago
source link: http://chuquan.me/2020/06/06/flutter-state-management-action/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

iOS 和 Android 的原生开发模式是命令式编程模式。命令式编程要求开发者一步步描述整个构建过程,从而引导程序去构建用户界面。

Flutter 则采用了声明式编程模式,框架隐藏了具体的构建过程,开发者只需要声明状态,框架会自动构建用户界面。这也就意味着 Flutter 构建的用户界面就是当前的状态。

aAZvuqI.png!web

状态管理

App 在运行中总是会更新用户界面,因此我们需要对状态进行有效的管理。状态管理本质上就是 如何解决状态读/写的问题 。对此,我们将从两个方面去评估状态管理方案:

  • 状态访问
  • 状态更新

此外,根据 Flutter 原生支持的情况,我们将 Flutter 状态管理方案分为两类:

  • Flutter 内置的状态管理方案
  • 基于 Pub 的状态管理方案

下文,我们将以 Flutter 官方的计数器例子来介绍 Flutter 中的状态管理方案,并逐步进行优化。

关于本文涉及的源码,见 【Demo 传送门】

Flutter 内置的状态管理方案

直接访问 + 直接更新

Flutter 模板工程就是【直接访问 + 直接更新】的状态管理方案。这种方案的状态访问/更新示意图如下所示。

Bn2UbuU.png!web

很显然,【直接访问 + 直接更新】方案只适合于在单个 StatefulWidget 中进行状态管理。那么对于多层级的 Widget 结构该如何进行状态管理呢?

状态传递 + 闭包传递

对于多层级的 Widget 结构,状态是无法直接访问和更新的。因为 Widget 和 State 是分离的,并且 State 一般都是私有的,所以子 Widget 是无法直接访问/更新父 Widget 的 State。

对于这种情况,最直观的状态管理方案就是:【状态传递 + 闭包传递】。对于状态访问,父 Widget 在创建子 Widget 时就将状态传递给子 Widget;对于状态更新,父 Widget 将更新状态的操作封装在闭包中,传递给子 Widget。

这里存在一个问题:当 Widget 树层级比较深时,如果中间有些 Widget 并不需要访问或更新父 Widget 的状态时,这些中间 Widget 仍然需要进行辅助传递。很显然,这种方案在 Widget 树层级较深时,效率比较低,只适合于较浅的 Widget 树层级。

uuYnYjy.png!web

状态传递 + Notification

那么如何优化多层级 Widget 树结构下的状态管理方案呢?我们首先从状态更新方面进行优化。

【状态传递 + Notification】方案采用 Notification 定向地优化了状态更新的方式。

通知(Notification)是 Flutter 中一个重要的机制,在 Widget 树种,每个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过 NotificationListener 来监听通知。Flutter 中将这种由子向父的传递通知的机制称为 通知冒泡 (Notification Bubbling)。通知冒泡和用户触摸事件冒泡是相似的,但有一点不同: 通知冒泡可以中止,而用户触摸事件无法中止

下图所示为这种方案的状态访问/更新示意图。

YbUFFfm.png!web

具体的实现源码如下所示:

// 与 父 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 都能够直接获取到共享的数据。

下图所示为这种方案的状态访问/更新示意图。

meqIZrJ.png!web

具体的源码实现如下所示:

/// 与父 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 监听到事件后进行状态更新。

下图所示为这种方案的状态访问/更新示意图。

7jEneyi.png!web

具体的源码实现如下所示:

/// 与父 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 获取到状态后,直接调用状态的更新方法呢?

q6b2Yru.png!web

对此,官方推荐了一套基于第三方 Pub 的 Provider 状态管理方案。

基于 Pub 的状态管理方案

Provider

【Provider】的本质是 基于 InheritedWidgetChangeNotifier 进行了封装 。此外,使用缓存提升了性能,避免不必要的重绘。

下图所示为这种方案的状态访问/更新示意图。

ERJbY3m.png!web

具体的源码实现如下所示:

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

ERJbY3m.png!web

在这个基础上, Redux 对于状态更新的过程进行了进一步的细分和规划 ,使得其数据的流动过程如下所示。

  • 所有的状态都存储在 Store 中。一般会把 Store 放在 App 顶层。
  • View 获取 Store 中存储的状态。
  • 当事件发生时,发出一个 action。
  • Reducer 接收到 action,遍历 action 表,找到匹配的 action,根据 action 生成新的状态存储到 Store 中。
  • Store 存储新状态后,通知依赖该状态的 view 更新。

一个 Store 存储多个状态,适合用于全局状态管理。

UZ7JBfJ.png!web

具体的实现源码如下所示。

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

ERJbY3m.png!web

【BLoC】方案的底层实现与【Provider】是非常相似的,也是基于 InheritedWidget 进行状态访问,并且对状态进行了封装,从而提供直接更新状态的方法。

但是,BLoC 的核心思想是 基于流来管理数据 ,并且将业务逻辑均放在 BLoC 中进行,从而实现视图与业务的分离。

  • BLoC 使用 Sink 作为输入,使用 Stream 作为输出。
  • BLoC 内部会对输入进行转换,产生特定的输出。
  • 外部使用 StreamBuilder 监听 BLoC 的输出(即状态)。

qaAz2iY.png!web

具体的实现源码如下所示。

/// 与父 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) - 数据传递/状态管理 | 掘金技术征文
  12. Flutter状态管理-初探与总结

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK