0.前言
响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”,在响应式编程下,我们只需要描述好UI和状态之间的关系,然后专注于状态的改变就好了,框架会根据状态的变化来自动更新UI。
在Flutter中,状态管理是十分重要的,因为Flutter的UI是声明式的,即UI是由当前状态决定的,而不是通过修改UI来改变状态,这就要求我们必须清晰地掌握和管理应用的状态,以确保UI的正确渲染和更新。良好的状态管理可以使开发者更高效地开发和维护代码。
1.什么是状态管理
在软件开发中,
状态是指程序或应用在运行过程中的各种数据和状态信息,包括用户输入、网络请求、页面展示等等。状态管理是指在应用中对这些状态进行管理的过程。
总结来说: 状态管理就是当某个状态发生改变的时候,告知使用该状态的状态监听者,让状态所监听的属性随之改变,从而达到联动效果。
2.状态分类
需要我们自己 管理 的状态可以分为两种概念类型:短时 (ephemeral) 状态和应用 (app) 状态。
- 2.1 短时状态(Ephemeral state)
某些状态、或是可以理解为某些数据只需要在当前的Widget中访问和使用,不需要对这些状态进行共享访问,你需要的只是一个StatefulWidget组件,依靠这个StatefulWidget组件自己的State类自己管理即可,不需要使用状态管理框架去管理这种状态,这些状态可以称之为短时状态。
如:官网中的计数器Demo、比如一个PageView组件记录当前的页面
- 2.2 应用状态(App state)
某些状态需要被组件共享访问,当这个状态发生变化的时候,其他组件也需要随之发生联动的变化,这就是应用状态。
举个例子来说明,比如一个电商App,在商品的详情页面,我们把某个商品加入了购物车,那么商品是否放入购物车这个状态,就需要被购物车页面组件所访问,那么这个状态就是应用状态。
试想一下,如果在不使用第三方状态管理框架的情况下,我们可以怎么实现呢,可以使用InheritedWidget定向的传递,可以通过Notification进行通知,可以使用event_bus来进行事件订阅等等,其实我们所说的状态管理框架,也是基于上面说的等几种方式来实现的。
3.管理状态的最常见的方法
思考一个问题:
StatefulWidget的状态应该被谁管理?Widget本身?父 Widget ?都会?还是另一个对象?
答案是取决于实际情况!以下是管理状态的最常见的方法:
- Widget 管理自己的状态。
- 父 Widget 管理子 Widget 状态。
- 混合管理(父 Widget 和子 Widget 都管理状态)。
如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理。
- 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理。
- 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。
在 Widget 内部管理状态封装性会好一些,而在父 Widget 中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父 Widget 中管理(灵活会显得更重要一些)。
我们将通过创建三个简单示例TapboxA、TapboxB和TapboxC来说明管理状态的不同方式。 这些例子功能是相似的:创建一个盒子,当点击它时,盒子背景会在绿色与灰色之间切换。状态
_active确定颜色:绿色为true,灰色为false。 如图:
3.1 Widget 管理自己的状态
示例:TapboxA 管理自身状态
/// TapboxA 管理自身状态.
//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
TapboxA({Key? key}) : super(key: key);
@override
_TapboxAState createState() => _TapboxAState();
}
/// 对应的_TapboxAState 类
/// - 管理TapboxA的状态。
/// - 定义`_active`:确定盒子的当前颜色的布尔值。
/// - 定义`_handleTap()`函数,该函数在点击该盒子时更新`_active`,并调用`setState()`更新UI。
- 实现widget的所有交互式行为。
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
3.2 父 Widget 管理子 Widget 状态
子Widget通过回调将其状态导出到其父Widget,状态由父Widget管理。
示例:
// ParentWidget 为 TapboxB 管理状态.
//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
/// ParentWidgetState 类:
/// - 为TapboxB 管理_active状态。
/// - 实现_handleTapboxChanged(),当盒子被点击时调用的方法。
/// - 当状态改变时,调用setState()更新UI。
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//------------------------- TapboxB ----------------------------------
/// TapboxB 类:
/// - 继承`StatelessWidget`类,因为所有状态都由其父组件处理。
/// - 当检测到点击时,它会通知父组件。
class TapboxB extends StatelessWidget {
TapboxB({Key? key, this.active: false, required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
3.3 混合状态管理
子Widget自身管理一些内部状态,而父Widget管理一些其他外部状态。
示例:当手指按下时,盒子的周围会出现一个深绿色的边框,抬起时,边框消失。点击完成后,盒子的颜色改变。 TapboxC 将其_active状态导出到其父组件中,但在内部管理其_highlight状态。这个例子有两个状态对象_ParentWidgetState和_TapboxCState。
//---------------------------- ParentWidget ----------------------------
class ParentWidgetC extends StatefulWidget {
@override
_ParentWidgetCState createState() => _ParentWidgetCState();
}
/// _ParentWidgetStateC类:
/// - 管理`_active` 状态。
/// - 实现 `_handleTapboxChanged()` ,当盒子被点击时调用。
/// - 当点击盒子并且`_active`状态改变时调用`setState()`更新UI。
class _ParentWidgetCState extends State<ParentWidgetC> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
TapboxC({Key? key, this.active: false, required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
@override
_TapboxCState createState() => _TapboxCState();
}
/// `_TapboxCState` 对象:
/// - 管理`_highlight` 状态。
/// - `GestureDetector`监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。
/// - 当按下、抬起、或者取消点击时更新`_highlight`状态,调用`setState()`更新UI。
/// - 当点击时,将状态的改变传递给父组件。
class _TapboxCState extends State<TapboxC> {
bool _highlight = false;
void _handleTapDown(TapDownDetails details) {
setState(() {
_highlight = true;
});
}
void _handleTapUp(TapUpDetails details) {
setState(() {
_highlight = false;
});
}
void _handleTapCancel() {
setState(() {
_highlight = false;
});
}
void _handleTap() {
widget.onChanged(!widget.active);
}
@override
Widget build(BuildContext context) {
// 在按下时添加绿色边框,当抬起时,取消高亮
return GestureDetector(
onTapDown: _handleTapDown, // 处理按下事件
onTapUp: _handleTapUp, // 处理抬起事件
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: Container(
child: Center(
child: Text(
widget.active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? Border.all(
color: Colors.teal[700],
width: 10.0,
)
: null,
),
),
);
}
}
4.Flutter中有哪些可以做到状态管理
4.1 State
常用而且使用频繁的一个状态管理类,它必须结合StatefulWidget一起使用,StreamBuilder继承自StatefulWidget,同样是通过setState来管理状态
State缺点:
- 无法做到跨组件共享数据(这个跨是无关联的,如果是直接的父子关系,我们不认为是跨组件) setState是State的函数,一般我们会将State的子类设置为私有,所以无法做到让别的组件调用State的setState函数来刷新。
- setState会成为维护的难点,因为啥哪哪都是。随着页面状态的增多,你可能在调用setState的地方会越来越多,不能统一管理。
- 处理数据逻辑和视图混合在一起,违反代码设计原则 比如数据库的数据取出来setState到Ui上,这样编写代码,导致状态和UI耦合在一起,不利于测试,不利于复用。
- setState是整个Widget重新构建(而且子Widget也会跟着销毁重建),如果页面足够复杂,就会导致严重的性能损耗。建议使用StreamBuilder,原理上也是State,但它做到了子Widget的局部刷新,不会导致整个页面的重建。
4.2 InheritedWidget
它的天生特性就是能绑定InheritedWidget与依赖它的子孙组件的依赖关系,并且当InheritedWidget数据发生变化时,可以自动更新依赖的子孙组件!
利用这个特性,我们可以将需要跨组件共享的状态保存在InheritedWidget中,然后在子组件中引用InheritedWidget即可。
专门负责Widget树中数据共享的功能型Widget,如Provider、scoped_model就是基于它开发的。
InheritedWidget缺点:
- 每次更新都会通知所有的子Widget,无法定向通知/指向性通知,容易造成不必要的刷新。
- 不支持跨页面(route)的状态,意思是跨树,如果不在一个树中,我们无法获取。
- 数据是不可变的,必须结合StatefulWidget、ChangeNotifier或者Steam使用。
4.3 Notification
它是Flutter中跨层数据共享的一种机制,注意,它不是widget,它提供了dispatch方法,沿着context对应的Element节点向上逐层发送通知。
- 不支持跨页面(route)的状态,准确说不支持NotificationListener同级或者父级Widget的状态通知。
- 本身不支持刷新UI,需要结合State使用。
- 如果结合State,会导致整个UI的重绘,效率低下不科学。
4.4 Stream
纯Dart的实现,跟Flutter没什么关系,扯上关系的就是用StreamBuilder来构建一个Stream通道的Widget,像知名的rxdart、BloC、flutter_redux、fish_redux全都用到了Stream的api。
- api生涩,不好理解。
- 需要定制化,才能满足更复杂的场景。
- 缺点恰恰是它的优点,保证了足够灵活,你更可基于它做一个好的设计,满足当下业务的设计。
5.常见的状态管理框架
5.1 Provider
Provider 是一个轻量级的状态管理框架,可用于单个 Widget 或整个 Widget 树中分发状态。它通过 InheritedWidget 和 ChangeNotifier 来实现状态管理,并支持依赖项注入。
- Provider是官方文档的例子用的方法. Google 比较推荐的用法. 和BLoC的流式思想相比, Provider是一个观察者模式, 状态改变时要notifyListeners().
- Provider的实现在内部还是利用了InheritedWidget,允许将有效信息传递到组件树下的小组件. Provider的好处: dispose指定后会自动被调用, 支持MultiProvider.
- Provider从名字上就很容易理解,它就是用于提供数据,无论是在单个页面还是在整个app 都有它自己的解决方案,可以很方便的管理状态。
常用概念:
- ChangeNotifier:系统提供的被观察者,数据model需要继承。
- Provider:订阅者,只用于数据共享管理,提供给子孙节点使用,UpdateShouldNotify Function,用于控制刷新时机。
- ChangeNotifierProvider:订阅者,不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有消费者。Model变化后会自动通知。ChangeNotifierProvider(订阅者),ChangeNotifierProvider内部会重新构建InheritedWidget,而依赖该InheritedWidget的子孙Widget就会更新。
- MultiProvider:多个订阅者:实际上就是通过每一个provider都实现了的 cloneWithChild方法把自己一层一层包裹起来。
- Consumer:消费者,能够在复杂项目中,极大地缩小你的控件刷新范围。多支持6种model。
- Selector: 消费者,强化的Consumer,支持过滤刷新。
使用流程:
- 添加依赖
- 创建数据 Model
- 创建顶层共享数据
- 顶层Provider包裹
- 在子页面中获取状态
Provder种类:
- Provider:只能提供恒定的数据,不能通知依赖它的子部件刷新。
- ListenableProvider: 提供的对象是继承了 Listenable 抽象类的子类,必须实现其 addListener / removeListener 方法,通常不需要。
- ChangeNotifierProvider: 对子节点提供一个继承/混入/实现了ChangeNotifier的类,只需要在Model中with ChangeNotifier ,然后在需要刷新状态时调用 notifyListeners 即可。
- ValueListenableProvider: 提供实现了继承/混入/实现了ValueListenable的Model,实际上是专门用于处理只有一个单一变化数据的ChangeNotifier。
- StreamProvider: 专门用作提供(provide)一条 Single Stream。
- FutureProvider:提供了一个 Future 给其子孙节点,并在 Future 完成时,通知依赖的子孙节点进行刷新。
总结:
本质上 Prvioder 通过 inheritedElement 实现局部刷新,通过控制自己实现的 Element 层来更新 UI,通过 Element 提供的 unmount 函数回调 dispose,实现选择性释放,其核心类:InheritedProvider。
Provider不仅做到了提供数据,而且它拥有着一套完整的解决方案,覆盖了你会遇到的绝大多数情况。就连BLoC未解决的那个棘手的dispose问题,和ScopedModel的侵入性问题,它也都解决了。它能够让你开发出简单、高性能、层次清 的应用。
不足之处:Flutter Widget 构建模式很容易在UI层面上组件化,但是仅仅使用Provider,Model和 View之间还是容易产生依赖。只有通过手动将Model转化为ViewModel这样才能消除掉依赖关系。
5.2 Redux
Redux 库是将状态和业务逻辑从 UI 中清晰分离的一种方式。它通过一个单一的状态存储库来管理应用程序的状态,并使用可预测的方式修改状态。
5.3 MobX
MobX 是一种基于响应式编程的状态管理框架,它使用观察者模式来观察和响应状态的变化,并可以自动地更新 UI。
5.4 BLoC
BLoC 是一种基于 Reactive Programming 和 Stream 的状态管理模式,它将应用程序的状态分为三层:Business Logic、View 和 UI。Business Logic 层负责逻辑处理,View 层负责渲染,UI 层则负责响应用户的操作。
5.5 GetX
GetX 是一个轻量级的状态管理框架,提供了路由、依赖注入、状态管理等功能,通过依赖注入和静态扩展,让您能更加方便地构建结构清晰、易于维护的架构。
5.6 优缺点对比
| 框架 | 优点 | 缺点 |
|---|---|---|
| Provider | 简单易用,轻量级;支持依赖项注入;方便快捷的状态管理。 | 难以处理大型应用中的复杂状态;不支持异步操作;共享状态跨 widget 树。 |
| Redux | 独立的状态管理,方便统一和管理;可预测且容易测试;支持中间件,方便处理异步操作。 | 学习成本较高;可能存在大量的样板代码;对于小型应用过于复杂。 |
| MobX | 响应式编程,易于理解和使用;自动化生成代码,方便快捷;扩展性很强。 | 状态分散,可能难以掌握应用的状态流;可能存在过多的注释和无用代码;需要加注解,使代码变得繁琐。 |
| BLoC | 适用于大型应用;规范的模式,方便维护;支持异步操作。 | 增加了代码复杂度;学习成本略高。 |
| GetX | 简单易用,轻便;提供完整的路由、依赖注入等功能;支持响应式编程。 | 在大型应用中,可能会难以管理依赖关系;响应式编程可能导致性能问题。 |
6.回顾总结
- 数据共享和同步:在应用程序中,不同部分可能需要共享和同步数据。通过状态管理,可以轻松地在应用程序的各个部分之间共享数据,并确保数据的一致性。
- UI更新:Flutter状态管理可以帮助开发者管理应用程序中的UI状态,以便在数据变化时更新用户界面。这样可以确保应用程序的UI与数据的状态保持同步。
- 复杂状态管理:随着应用程序变得越来越复杂,管理应用程序的状态变得更加困难。Flutter状态管理工具可以帮助开发者更有效地管理应用程序的状态,使代码更具可维护性和可扩展性。
- 性能优化:有效的状态管理可以帮助应用程序避免不必要的重绘和重新构建,从而提高应用程序的性能和响应速度。
- 代码结构:通过良好的状态管理,开发者可以更好地组织应用程序的代码结构,使其更易于理解和维护。