你不需要那么多Provider------重新理解状态管理与业务逻辑
状态管理一直是Flutter的热门话题, 而Provider/Riverpod更是Flutter官方的Favorite. 然而, 你真的需要这么多基于Widget树/BuildContext的状态管理吗?
本文将围绕Provider的核心机制, 重新审视状态与业务逻辑的本质, 并探讨全局状态管理的优势与潜在问题, 带你找到更优雅的状态管理方式.
一、Provider的核心: InheritedWidget与Widget树
Provider的核心在于利用Flutter的InheritedWidget机制, 通过Widget树实现数据的分发与访问. 它的关键优势是: 底层Widget可以向上查找, 找到Widget树中离自己最近的匹配数据. 这种机制非常适合需要根据Widget树位置动态获取数据的场景.
典型场景1: 主题样式
以主题样式为例, 假设我们在Widget树的顶层和中层分别放置了两个不同的ThemeData
实例:
dart
Widget build(BuildContext context) {
return Provider<ThemeData>(
create: (_) => ThemeData(primaryColor: Colors.blue),
child: Column(
children: [
// 使用顶层主题(蓝色)
MyWidget(),
Provider<ThemeData>(
create: (_) => ThemeData(primaryColor: Colors.red),
child: MyWidget(), // 使用中层主题(红色)
),
],
),
);
}
在这个例子中, 顶层以上的Widget会使用蓝色主题, 而中层以下的Widget会使用红色主题. 这种基于Widget树位置的数据查找机制, 让主题管理变得直观且高效.
典型场景2: 国际化
再来看一个更复杂的例子: 一个外语学习App, 用户交互页面需要使用用户的母语(例如中文), 而内部的教学页面需要使用教学语言(例如英文). 通过Provider, 我们可以在Widget树的不同层级放置不同的语言配置:
dart
Widget build(BuildContext context) {
return Provider<Locale>(
create: (_) => Locale('zh', 'CN'), // 顶层: 中文
child: Scaffold(
body: Column(
children: [
// 使用中文
UserInteractionWidget(),
Provider<Locale>(
create: (_) => Locale('en', 'US'), // 中层: 英文
child: TeachingWidget(), // 使用英文
),
],
),
),
);
}
通过这种方式, App可以根据Widget树的位置实现语言的自动切换, 非常适合需要动态区域化配置的场景.
二、效率的矛盾: 状态数据与Widget树无关
尽管Provider在主题和国际化等场景中表现出色, 但实际开发中需要管理的状态往往是用户操作数据, 例如表单输入、计数器数值等. 这些数据的特点是: 它们与Widget树的具体位置无关.
Counter应用的例子
以经典的Counter应用为例, 无论是将"+1"按钮放在AppBar中, 还是放在页面的Body内部, 其业务逻辑始终是: 点击按钮后, 计数器数值加1:
dart
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中
dart
// 存储全局状态
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;