你不需要那么多Provider——重新理解状态管理与业务逻辑

281 阅读3分钟

你不需要那么多Provider——重新理解状态管理与业务逻辑

状态管理一直是Flutter的热门话题, 而Provider/Riverpod更是Flutter官方的Favorite. 然而, 你真的需要这么多基于Widget树/BuildContext的状态管理吗?

本文将围绕Provider的核心机制, 重新审视状态与业务逻辑的本质, 并探讨全局状态管理的优势与潜在问题, 带你找到更优雅的状态管理方式.

一、Provider的核心: InheritedWidget与Widget树

Provider的核心在于利用Flutter的InheritedWidget机制, 通过Widget树实现数据的分发与访问. 它的关键优势是: 底层Widget可以向上查找, 找到Widget树中离自己最近的匹配数据. 这种机制非常适合需要根据Widget树位置动态获取数据的场景.

Provider优势场景: 国际化

对于一个用户交互页面需要使用母语(例如中文), 而内部的教学页使用教学语言(例如英文)的一个外语学习App(绿色猫头鹰). 就可以通过Provider在不同层级放置不同的Locale:

Widget build(BuildContext context) {
  return Provider<Locale>(
    create: (_) => Locale('zh', 'CN'), // 顶层: 中文
    child: Scaffold(
      body: Column(
        children: [
          // 用户UI区域使用中文
          UserInteractionWidget(),
          Provider<Locale>(
            create: (_) => Locale('en', 'US'), // 中层: 英文
            child: TeachingWidget(), // 内部教学Widget使用英文
          ),
        ],
      ),
    ),
  );
}

通过这种方式, App可以根据Widget树的位置实现语言的自动切换, 非常适合需要动态区域化配置的场景.

二、效率的矛盾: 状态数据与Widget树无关

尽管Provider在国际化等场景中表现出色, 但实际开发中需要管理的状态往往是用户操作数据, 例如表单输入、计数器数值等. 这些数据的特点是: 它们与自身在Widget树中的位置无关.

Counter应用的例子

以经典的Counter应用为例, 无论是将 “+1”按钮 放在AppBar中, 还是放在页面的Body内部, 其业务逻辑始终是: 点击按钮后, 计数器数值加1:

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () => context.read<Counter>().increment(),
          ),
        ],
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => context.read<Counter>().increment(),
          child: Text('Add'),
        ),
      ),
    );
  }
}

无论按钮位于Widget树的哪个位置, 计数器的逻辑都不会改变. 这表明, 业务逻辑的核心是数据本身, 而不是数据的分发方式.

三、从局部到全局几乎是一种必然

显而易见的是: 随着业务需求变化, 业务组件(“+1”按钮)会四处移动, 必然导致存储count数据的Provider<Counter>最终移动到Widget树的顶层. <Counter>从事实上变成了全局变量(尽管会有人试图在进入/离开特定页面时将其加载/释放)

既然所有的业务逻辑组件最终都会让状态Provider上移到App顶层, 那么为什么不一步到位, 从一开始就将状态放在全局变量中呢?

业务逻辑与UI组件分离

例如, 我们可以将计数器的状态放置在全局Map中

// 存储全局状态
final global_state_map = {};

class CounterPage extends StatelessWidget {
  @override
  void initState() {
    // 初始化全局状态
    global_state_map['count'] = 0;
    super.initState();
  }
  @override
  void dispose() {
    // 清理页面相关的全局状态
    global_state_map.remove('count');
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () => global_state_map['count']+=1,
          ),
        ],
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => global_state_map['count']+=1,
          child: Text('Add'),
        ),
      ),
    );
  }
}

当然, 移除Provider后又涉及到刷新后对UI的通知方案. 以上代码也仅仅是演示, 实际上可以通过存储Stream+StreamBuilder等方案来实现页面刷新. 在后续的文章中, 将会介绍完整的方案.

四、总结

Provider的InheritedWidget机制为国际化等场景提供了优雅的解决方案. 但更常见的业务场景中, 业务逻辑相关的数据没有必要与Widget树耦合. 受限于篇幅, 具体的开发方案敬请期待后续文章.

心急的朋友可以直接查看 HiveState 仓库: github.com/Hu-Wentao/h… 示例与example均基本完备, 选择web浏览器设备即可运行web demo;