状态管理
状态管理是前端开发领域上常用的一门技术,前端开发一直很注重 UI 和业务数据的独立性,即数据源的唯一性和数据的单向流动性。
在移动开发领域,比如iOS, 一开始是以 MVC 框架来作为 UI 和 业务数据的构建,并没有状态管理相应的概念,分别是 Model、Controller 和 View:
- 用户通过 View 上的 Action 触发逻辑回调给 Controller
- 通过 Controller 去更新 Model 中的数据
- Model 数据更新后,通知 View 刷新 UI
MVC 如果按规范使用起来,其实也可以实现数据源的单一和数据的单向流动性,但实际应用中却存在诸多的不利因素:
- 缺乏明确的定义和规范
- Controller 除了管理 Views,还要管理 Models,到后期会过于臃肿
- Controller 和 View 的关联性过强
- 等等
而 Flutter,在一推出的时候就引入状态管理来,通过这强有力状态管理来实现数据源的单一性和数据流的单向性。
状态管理
那什么才是状态管理?
状态管理,是用单一数据流的思想指导整个系统,把状态存储在特定的地方,在 UI 组件层监听数据的改变,并刷新 UI,通过 Action 触发数据更新。
状态管理的优点
- 能有效分离 UI 层和数据处理层
- 有效控制状态变化和 UI 的刷新
- 在业务组织上更加解耦
- 代码维护性和稳定性更高
Flutter 的状态管理方案
Flutter 作为一个响应式框架,状态管理是 Flutter 开发过程中代码架构的重点,官方和开发者提供了很多 Flutter 状态管理相关的优秀开源库,不同的技术方案有不同的落地场景。
State 和 InheritedWidget
State 和 InheritedWidget 是 Flutter 中最基础的状态管理方案。
在 Flutter 中,Widget 是不可变的,但 State 是支持跨帧保存信息,当开发者执行 setState 方法时,State 内部会通过调用 markNeedBuild 方法,将 State 对应的 Element 标记为 diry,从而下一帧会执行 WidgetsBinding.drawFrame 时更新界面信息。
InheritedWidget 在 Flutter 中用于数据共享,可以实现数据从上往下传递共享,child 通过 BuildContext 可以往上获取 InheritedWidget 中的数据。比如:Material库里面的主题管理(Theme), Scoped Model, Redux 都是使用这个机制来实现数据共享。
通过继承 InheritedWidge,用于绑定依赖和控制刷新通知:
class InheritedText extends InheritedWidget {
final String text;
const InheritedText({Key? key, required this.text, required Widget child})
: super(child: child, key: key);
//该回调决定当data发生变化时,是否通知子树中依赖data的Widget
@override
bool updateShouldNotify(covariant InheritedText oldWidget) {
return text != oldWidget.text;
}
//定义一个便捷方法,方便子树中的widget获取共享数据
static InheritedText? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<InheritedText>();
}
}
实现一个子组件 TextWidget,在 build 方法中引用 InheritedText 中的数据:
class TextWidget extends StatelessWidget {
const TextWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
width: 200,
height: 100,
color: Colors.redAccent,
child: Text(
InheritedText.of(context)?.text ?? '',
),
);
}
}
最后创建一个按钮,更新数据:
class _StateManagerPageState extends State<StateManagerPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SafeArea(
child: Center(
child: InheritedText(
text: _count.toString(),
child: const TextWidget(),
),
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
setState(() {
_count++;
});
},
),
);
}
}
Stream
Stream 在 Flutter 中也是经常使用到,但它却不是 Flutter 设计的,而是来自 Dart 的封装,一般它和 Future 用于实现异步的逻辑。
Future 一般我们常用于单个异步实现,而 Stream 是用于多个异步实现,以数据流的方式存在,不仅如此,Stream 可以支持同步或异步。
在 Flutter 的状态管理设计中,除了 State 和 InheritedWidget,其他的像 rxdart、BLoc、flutter_redux、Provider 都离不开 Stream 的封装。
- 在Flutter的Stream流中,对外暴露的对象主要有StreamController、Sink、Stream和StreamSubscription
- StreamController:如类名描述,用于控制整个Stream的过程,提供各类接口用于创建各种事件流。
- StreamSink:一般作为事件的入口,提供如add、addStream等方法。
- Stream:事件源本身,一般可用于监听事件流或者对事件进行转换,如 listen、where等方法。
- StreamSubscription:事件订阅后得到的对象,表面上用于管理订阅过的各类操作,如cancel、pause等操作,同时在事件流的内部也是事件中转的关键。
Stream 的广播和非广播
在 Stream 中有广播 _BroadcastStreamController 和 非广播 _StreamController 区分:
- 非广播只允许设置一个监听器,并且只有在 Stream 设置监听器才开始产生事件,取消监听器则停止发送事件,并且也不允许在流上设置其他监听器
- 广播则允许设置多个监听器
Stream 的事件转换
Stream 支持对事件进行变换的处理,通过变化可以经过筛选和多次处理,从而最终达到需要的结果,如 map、where、elementAt 等。
Stream 的简单使用
Stream的使用方式:
- 首先创建StreamController对象
- 通过StreamController获取StreamSink对象用于事件入口
- 通过StreamController获取Stream对象用于监听数据变化
- 通过Stream的listen监听得到StreamSubscription对象用于管理订阅
创建 StreamController,StreamController 有多个生命周期,如 cancel、listen、pause、resume 等:
final StreamController<int> _streamController = StreamController(
onCancel: () {
debugPrint('_streamController onCancel');
},
onListen: () {
debugPrint('_streamController onListen');
},
onPause: () {
debugPrint('_streamController onPause');
},
onResume: () {
debugPrint('_streamController onResume');
},
sync: true,
);
通过 StreamSubscription 对 Stream 数据流进行监听,并刷新 UI:
_streamSubscription = _streamController.stream.listen((event) {
setState(() {
_count += event;
});
});
通过 Sink 添加新的数据:
_streamController.sink.add(1);
RxDart
从订阅或者变换的角度可以看出,Stream 都已经拥有了 ReactiveX 的设计思想,为了让开发者更加方便的使用 Stream,ReactiveX 为 Dart 语言封装了 RxDart 来满足对 ReactiveX 的熟悉感,所以 RxDart 是 Stream 的拓展。
| Dart | RxDart |
|---|---|
| StreamController | Subject |
| Stream | Observable |
在RxDart,Observable是一个Stream,而Subject继承了Observable,所以也是一个Stream,并且还实现了StreamController的接口,所以具备Controller和Stream的两种作用。
RxDart 的使用流程总结:
- PublishSubject 内部创建了一个广播 StreamController.broadcast,add 或者 addStream 最终都会调用 StreamController.add
- 当调用 onListen 时,也是将回调设置回 StreamController 中
- RxDart 在做变换时,也是基于创建时传入的 Stream 对象进行变换
RxDart 只是对 Stream 进行了概念的转换,本身还是 Stream。
首先创建一个被观察者 Subject:
final PublishSubject<int> _subject = PublishSubject<int>();
通过 StreamSubscription 对 Subject 数据流进行监听,并刷新 UI:
_streamSubscription = _subject.stream.listen((event) {
setState(() {
_count += event;
});
});
Subject 更新数据:
_subject.add(1);
scoped_model
scoped_model 是 Flutter 最为简单的第三方状态管理框架。
scoped_model 的使用
scoped_model 的实现状态管理的三步:
- 定义 Model 的实现,且在状态改变时执行 notifyListeners 方法
- 使用 ScopedModel 控件加载 Model
- 使用 ScopedModelDescendant 或者 ScopedModel.of(context) 加载 Model 内状态数据进行显示
首先创建一个 CountModel,继承 Model,并且实现一个 add 方法用于更新模型中的数据:
class CountModel extends Model {
int _count = 0;
int get count => _count;
void add(int value) {
_count += value;
// 更新数据
notifyListeners();
}
}
使用 ScopedModel 控件初始化 CountModel,并且使用 ScopedModelDescendant 或 ScopedModel.of(context) 获取 Model 内容并显示:
return ScopedModel<CountModel>(
model: CountModel(),
child: Builder(
builder: (context) {
return Scaffold(
appBar: AppBar(
title: const Text('scoped_model'),
),
body: const SafeArea(
child: Center(child: TextWidget()),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
var model = ScopedModel.of<CountModel>(context);
model.add(
Random().nextInt(10),
);
},
child: const Icon(Icons.add),
),
);
}
),
);
ScopedModel 的原理
- Model实现了Listenable接口,内部维护一个Set_listeners
- 当Model设置给AnimatedBuilder时,Listenable的addListener会被调用,然后添加一个_handleChange监听到 _listeners这个Set中
- 当Model调用notifyListeners,会通过异步调用scheduleMicrotask执行_listeners中的/_handleChange、
- _handleChange监听被调用,执行setState({})更新界面
Flutter_redux
redux 一般是前端开发常用的状态管理框架,在 redux 中,数据都存在单一的可信源中,叫 Store,然后数据储存时通过 Reducer 更新,而触发这些更新动作就是 Action。
- Store 是数据存储仓库,保存所有数据,Flutter 一般建议只配置一个 Store
- Action 代表一种操作,Redux 不允许世界操作 Store 里面的数据,Action 相当于一个 Store 请求
- reducer 是真正修改 Store 的执行者,一般只有两个参数,一个是当前的 State,另一个是 Action
- middleware 是中间件,一般可以拦截或做 AOP 等
- 将 Store 设置给 StoreProvider 用于状态共享
- 通过 StoreConnector 或 StoreBuilder 加载显示 Store 中的数据
class ReduxPage extends StatelessWidget {
ReduxPage({Key? key}) : super(key: key);
late final Store<CountState> _store = Store<CountState>(
(state, action) => addReducer(state, action),
initialState: CountState(count: 0),
middleware: [CountMiddleware()]);
@override
Widget build(BuildContext context) {
return StoreProvider(
store: _store,
child: Scaffold(
appBar: AppBar(
title: const Text('Flutter redux 使用'),
),
body: const SafeArea(
child: CountWidget(),
),
),
);
}
}
class CountWidget extends StatelessWidget {
const CountWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreBuilder<CountState>(builder: (context, countState) {
String text = countState.state.count.toString();
return Column(
children: <Widget>[
Expanded(
child: Center(
child: Text(text),
),
),
Row(
children: <Widget>[
Expanded(
child: FloatingActionButton(
onPressed: () {
countState.dispatch(Actions.addAction);
},
child: const Icon(Icons.add),
),
),
Expanded(
child: FloatingActionButton(
onPressed: () {
countState.dispatch(Actions.decAction);
},
child: const Icon(Icons.remove),
),
)
],
),
],
);
});
}
}
class CountState {
CountState({required this.count});
final int count;
}
// Reducer
CountState addReducer(CountState state, dynamic action) {
if (action == Actions.addAction) {
return CountState(count: _addHandler(state.count, action));
} else {
return CountState(count: _decHandler(state.count, action));
}
}
int _addHandler(int count, Actions action) {
count++;
return count;
}
int _decHandler(int count, Actions action) {
count--;
return count;
}
enum Actions {
addAction,
decAction,
}
class CountMiddleware implements MiddlewareClass<CountState> {
@override
call(Store<CountState> store, action, NextDispatcher next) {
if (action == Actions.addAction) {
debugPrint('Actions.addAction');
} else if (action == Actions.decAction) {
debugPrint('Actions.decAction');
}
next(action);
}
}
redux 的内部实现逻辑,是对 Stream、SteamBuilder 和 InheritedWidgets 的自定义封装。开发者每个操作都是一个 Action,而触发逻辑完全由 middleware 和 reducer 决定,并且整个流程是单向的,将业务和 UI 进行了隔离,规范了事件流过程中的规范。
Flutter Provider
Flutter Provider 是 Flutter 推荐的状态管理方式之一,特点是不复杂、好理解、可控度高。
Flutter Provider 包里面有很多 Provider 类型。
Provider
Provider 是最基本类型,它可以包括为所有子 Widget 提供值,但却不能更新 widget。
创建一个 CountModel:
class CountModel {
int _count = 0;
int get count => _count;
void add(int value) {
_count += value;
//notifyListeners();
}
}
接着创建一个 Provider,并初始化 model:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Provider'),
),
body: SafeArea(
child: Provider<CountModel>(
create: (context) {
return model;
},
child: const Center(child: TextWidget()),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
model.add(1);
},
child: const Icon(Icons.add),
),
);
}
}
虽然我们触发点击更新数值,但它是无法更新到 UI 的。
ChangeNotifierProvider
ChangeNotifierProvider 会监听提供出去的模型对象中的值更改,当值发生更改,会重建下方所有的 Consumer 和使用 Provider.of(context) 监听并获取值的地方。
首先 CountModel 需要继承 ChangeNotifier,并且在数据发生更改时要调用 notifyListeners 方法:
class CountModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void add(int value) {
_count += value;
notifyListeners();
}
}
接着通过 ChangeNotifierProvider 包裹子 Widget,通过 Consumer 或 Provider.of(context) 监听数据更改和获取数据:
child: ChangeNotifierProvider<CountModel>(
create: (context) {
return model;
},
child: const Center(child: TextWidget()),
)
class TextWidget extends StatelessWidget {
const TextWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
var model = Provider.of<CountModel>(context);
return Container(
alignment: Alignment.center,
width: 200,
height: 100,
color: Colors.redAccent,
child: Text(
model.count.toString(),
),
);
}
}
FutureProvider
FutureProvider 是普通的 FutureBuilder 的包装,FutureProvider 初始化时需提供一个初始化值和延时值,当获取到延时值时会重新构建当前 UI,但却无法监听后续数据的更改。
FutureProvider 使用的 create 是一个返回 Future 类型的方法,可在里面进行延时请求,并需要提供初始数据:
child: FutureProvider(
create: (context) async {
await Future.delayed(const Duration(seconds: 2));
return CountModel(count: 2);
},
initialData: CountModel(count: 1),
child: const Center(child: TextWidget()),
),
StreamProvider
StreamProvider 是 StreamBuilder 的包装,和 FutureProvider 一样,无法监听模型数据的更改。
ValueListenableProvider
ValueListenableProvider 和 ChangeNotifierProvider 一样,在值改变的时候可以通知子 widget 重新构建 UI,但使用起来更加复杂,需要先用 Provider 提供 Model 给 Consumer。
首先构建一个 Model,Model 中有一个 ValueNotifier,提供给 ValueListenableProvider 使用:
class CountModel {
ValueNotifier<int> counter = ValueNotifier(0);
}
接着需要先用 Provider 提供 Model,用 Consumer 包 ValueListenableProvider:
return Provider(
create: (context) => CountModel(),
child: Consumer<CountModel>(
builder: (context, model, child) {
return ValueListenableProvider<int>.value(
value: model.counter,
child: Scaffold(
appBar: AppBar(
title: const Text('Provider'),
),
body: Consumer<int>(
builder: (context, value, child) {
return const SafeArea(
child: Center(child: TextWidget()),
);
},
),
floatingActionButton: Consumer<CountModel>(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
model.counter.value += 1;
},
child: const Icon(Icons.add),
);
},
),
),
);
},
),
);
}
ListenableProvider
ListenableProvider和ChangeNotifierProvider一样, 区别在于,如果Model是一个复杂模型ChangeNotifierProvider 会在你需要的时候,自动调用其 _disposer 方法,所以一般还是使用ChangeNotifierProvider即可。
MultiProvider
上面的这些例子都是用到单一模型,但一般在业务迭代,我们可能会用到多个模型,虽然可以通过嵌套来解决这样的需求,但嵌套毕竟多了有点恶心 MultiProvider 支持多个 providers 进行组合,解决嵌套问题。
声明 MultiProvider,并且在 providers 中放入多个 model:
return MultiProvider(
providers: [
ChangeNotifierProvider<BannerModel>(create: (context) => BannerModel()),
ChangeNotifierProvider<ListModel>(create: (context) => ListModel()),
],
....
}
之后便可以同时监听多个 Model 的内容变化,从而驱动 UI 刷新。
ProxyProvider
如果要提供两个Model,但是其中一个Model取决于另一个Model,在这种情况下,可以使用ProxyProvider。A ProxyProvider从一个Provider获取值,然后将其注入另一个Provider,
比如我们上传图片会将图片先上传到阿里云等服务器,拿到 URL,再把 URL 传给自己的服务器:
return MultiProvider(
providers: [
ChangeNotifierProvider<PicModel>(create: (context) => PicModel()),
ProxyProvider<PicModel, SubmitModel>(
update: (context, myModel, anotherModel) => SubmitModel(myModel),
),
],
...
Consumer<PicModel>(
builder: (context, model, child) {
return TextButton(
onPressed: model.upLoadPic,
child: const Text("提交图片"),
);
},
),
Consumer<PicModel>(
builder: (context, model, child) {
return TextButton(
onPressed: model.upLoadPic,
child: const Text("提交图片"),
);
},
),
...
}
Flutter 状态管理的区别
scoped_model 比较适合新手和小型轻度的项目,而futter_redux、Provider更适合中大型项目。在挑选框架时,futter_redux更适合有前端开发经验的人,而Provider更适合拥有App开发经验的人使用。Provider、futter_redux 会更倾向于用在需要数据共享或者相对复杂的业务封装中,比如登录状态、用户信息、主题和多语言切换等。