在前端开发中,我们经常能听到 redux 等状态管理的词汇。
但是对于我这种搞移动端出身的人,对这些词汇就不是很熟悉。
Flutter 作为借鉴了很多 React 思想的语言,自然也会有相对应的状态管理。
那什么是状态管理?为什么需要状态管理?
什么是状态管理?
个人认为 状态管理解决的是组件之间的通讯以及状态集中管理和分发的问题
举个例子:
比如我多个页面同时使用了 User 对象,当我其中一个地方改了以后,想要其他的地方也都要更改,那这个时候就需要状态管理来集中管理数据。
为什么需要状态管理?
前面已经说过一点,另一点:
我们已经使用过 StatefulWidget,也知道它维护了一个 State,也就是当前 Widget的状态。
当我们需要改变 Widget 的状态的时候,就需要 setState(),这样就会重新走一遍 build 方法来重绘。
当页面简单的时候还好说,如果页面复杂了,我们每次点击、或者滑动都要来进行整个页面的 build 吗?
很明显,这样不符合常理。
相信很多人已经听过 provide
redux
... 等等状态管理的方案,
那么 Scoped_Model 是什么?
Scoped_Model
先看一下Scoped_Model GitHub 文档上的内容:
A set of utilities that allow you to easily pass a data Model from a parent Widget down to it's descendants. In addition, it also rebuilds all of the children that use the model when the model is updated. This library was originally extracted from the Fuchsia codebase. 一组实用程序,允许您轻松地将数据模型从父窗口小部件传递给它的后代。此外,它还重建了模型更新时使用模型的所有子代。这个库最初是从 Fuchsia 基代码中提取的。
和其他的状态管理一样,它也是使用的 InheritedWidget, 利用 InheritedWidget 来管理使用了该数据的Widget。
这样就可以在数据改变的时候更新该 Widget 了。
简单使用 Scoped_Model
来看一下官方给出的Demo:
代码语言:javascript复制import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
void main() {
runApp(MyApp(
model: CounterModel(),
));
}
class MyApp extends StatelessWidget {
final CounterModel model;
const MyApp({Key key, @required this.model}) : super(key: key);
@override
Widget build(BuildContext context) {
// At the top level of our app, we'll, create a ScopedModel Widget. This
// will provide the CounterModel to all children in the app that request it
// using a ScopedModelDescendant.
return ScopedModel<CounterModel>(
model: model,
child: MaterialApp(
title: 'Scoped Model Demo',
home: CounterHome('Scoped Model Demo'),
),
);
}
}
// Start by creating a class that has a counter and a method to increment it.
//
// Note: It must extend from Model.
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
// First, increment the counter
_counter ;
// Then notify all the listeners.
notifyListeners();
}
}
class CounterHome extends StatelessWidget {
final String title;
CounterHome(this.title);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
// Create a ScopedModelDescendant. This widget will get the
// CounterModel from the nearest parent ScopedModel<CounterModel>.
// It will hand that CounterModel to our builder method, and
// rebuild any time the CounterModel changes (i.e. after we
// `notifyListeners` in the Model).
ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return Text(
model.counter.toString(),
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
// Use the ScopedModelDescendant again in order to use the increment
// method from the CounterModel
floatingActionButton: ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return FloatingActionButton(
onPressed: model.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
),
);
}
}
代码有点长,但是没关系,大部分都是注释,不管那么多,
我们直接copy代码到项目中,运行看一下效果:
效果非常简单,和我们刚开始学Flutter一样的例子。
下面就解释一下代码,
可以看到,首先是把 ScopedModel 放在了APP 最顶部来初始化:
代码语言:javascript复制class MyApp extends StatelessWidget {
final CounterModel model;
const MyApp({Key key, @required this.model}) : super(key: key);
@override
Widget build(BuildContext context) {
// At the top level of our app, we'll, create a ScopedModel Widget. This
// will provide the CounterModel to all children in the app that request it
// using a ScopedModelDescendant.
return ScopedModel<CounterModel>(
model: model,
child: MaterialApp(
title: 'Scoped Model Demo',
home: CounterHome('Scoped Model Demo'),
),
);
}
}
随后定义了一个 CounterModel:
代码语言:javascript复制// Start by creating a class that has a counter and a method to increment it.
//
// Note: It must extend from Model.
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
// First, increment the counter
_counter ;
// Then notify all the listeners.
notifyListeners();
}
}
注释上面写的很清楚,必须继承自 Model
。
为什么?我们看Model源码:
代码语言:javascript复制abstract class Model extends Listenable {
final Set<VoidCallback> _listeners = Set<VoidCallback>();
int _version = 0;
int _microtaskVersion = 0;
/// [listener] 将在Model更改时调用。
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
/// [listener] 移除时调用。
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
/// Returns the number of listeners listening to this model.
int get listenerCount => _listeners.length;
/// 仅当Model已更改时由[model]调用。
@protected
void notifyListeners() {
// 我们安排一个微任务来消除可能同时发生的多个更改。
if (_microtaskVersion == _version) {
_microtaskVersion ;
scheduleMicrotask(() {
_version ;
_microtaskVersion = _version;
// Convert the Set to a List before executing each listener. This
// prevents errors that can arise if a listener removes itself during
// invocation!
_listeners.toList().forEach((VoidCallback listener) => listener());
});
}
}
}
可以看到,Model 继承了 Listenable,所以我们在自己定义 Model 的时候才可以调用 notifyListeners()
方法。
最后在需要该 Model的地方使用 ScopedModelDescendant
来获取。
ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return Text(
model.counter.toString(),
style: Theme.of(context).textTheme.display1,
);
},
),
有人可能觉得这种方式不是很优雅,代码太多。
官方也提供了另一种方法: ScopedModel.of<CounterModel>(context)
。
状态的集中管理以及 Widget更新
官方示例只是提供一个简单的例子,并不能展现出它的威力,
所以我们自己写一个示例。
该示例在多个页面同时使用同一个数据,然后在其中一个页面更新数据。
这样就达到了我们所谓状态的集中管理。
效果如下:
主要代码如下:
代码语言:javascript复制// 点击事件
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: ScopedModel.of<ScopedCounter>(context).increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
);
}
// 接收事件
class CounterLabel extends StatelessWidget {
const CounterLabel({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print("third counter label build");
final counter = ScopedModel.of<ScopedCounter>(context, rebuildOnChange: true);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${counter.count}',
style: Theme.of(context).textTheme.display1,
),
],
);
}
}
可以看到我们的 Widget 都是无状态的,也就是说我们确实达到了数据更新就更新UI的要求。
那么我们再打印log 看一下,是否只是更新了 使用该 Model 的 Widget。
还是整个Page 都 build 了。
我们在 Page 的 build方法中打印:
代码语言:javascript复制class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print("home page build");
}
}
........ // 第二第三页同理
print("second home page build");
print("third counter label build");
然后在 CounterLabel 中 打印
代码语言:javascript复制class CounterLabel extends StatelessWidget {
const CounterLabel({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print("home counter label build");
}
}
........ // 第二第三页同理
print("second counter label build");
print("third counter label build");
运行效果如下:
可以看到,确实只更新了使用该 Model 的 Widget。
总结
在Flutter 中状态管理有很多,redux、fish_redux 等等等等。
而Scoped_Model 是我用过最简单,最舒服的一种。
因为我是搞移动开发的,所以我会选择 Scoped_Model。
下一篇简单讲讲 Scoped_Model 的原理。
完整代码已经传至GitHub:https://github.com/wanglu1209/WFlutterDemo