本文示例代码
数据共享 InheritedWidget
InheritedWidget
是 Flutter 中非常重要的一个功能型组件,它提供了一种数据在 widget 树中从上到下传递的方式。例如在根 Widget 中通过 InheritedWidget
共享了一个数据,那么我们就可以在任意的子 Widget 中获取改共享的数据;
这个特性在一些需要 widget 树中共享数据的场景非常方便,如 Fluter SDK 正是通过该 Widget 来共享应用主题 和 Locale(语言环境)信息的;
didChangeDependencies
该回调是 State 对象的,他会在依赖发生变化时被 Flutter Framework 调用,这个依赖指的就是 widget 是否使用了父 widget 中的 InheritedWidget 的数据;
如使用了,则代表该组件依赖 InheritedWidget,如果没有使用,则代表没有依赖。
这种机制可以使子组件所依赖的 InheritedWidget 在变化时来更新自身,例如主题,等发生变化的时候,依赖的子 widget 的 didChangeDependencies
方法就会被调用
下面看一个栗子:
代码语言:javascript复制class ShareDataWidget extends InheritedWidget {
//需要共享的数据
final int data;
ShareDataWidget({@required this.data, Widget child}) : super(child: child);
//定义一个便捷的方法,方便子树中的 widget获取共享数据
static ShareDataWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType();
}
///该回调决定当 data 发生变化的时候,是否通知子树中依赖 data 的 widget
@override
bool updateShouldNotify(covariant ShareDataWidget oldWidget) {
//返回true:则子树中依赖当前 widget 的 widget 的 didChangeDependencies 会被调用
return oldWidget.data != data;
}
}
复制代码
上面定义了一个共享的 ShareDataWidget ,它继承自 InheritedWidget,保存了一个 data 属性,data 属性就是需要共享的数据
代码语言:javascript复制class TestShareWidget extends StatefulWidget {
@override
_TestShareWidgetState createState() => _TestShareWidgetState();
}
class _TestShareWidgetState extends State<TestShareWidget> {
@override
Widget build(BuildContext context) {
return Text(ShareDataWidget.of(context).data.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('发生改变');
}
}
复制代码
上面实现了一个子组件,在 build 方法中使用了 ShareDataWidget 的数据,同时在回调中打印了日志
最后,创建一个按钮,点击一次,就让 ShareDataWidget 的值自增
代码语言:javascript复制class TestInheritedWidget extends StatefulWidget {
@override
_TestInheritedWidgetState createState() => _TestInheritedWidgetState();
}
class _TestInheritedWidgetState extends State<TestInheritedWidget> {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: ShareDataWidget(
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: TestShareWidget(),
),
RaisedButton(
child: Text("Increment"),
//每点击一次,count 自增,然后重新 build,ShareDataWidget 将被更新
onPressed: () => setState(() => count),
)
],
),
),
);
}
}
复制代码
效果如下:
可见,依赖发生变化之后,子组件的 did... 方法就会被调用。需要注意的是如果 TestShareWidget 的 build 方法中没有使用 ShareDataWidget 的数据,那么他的 did... 方法将不会调用,因为他并没有依赖 ShareDataWidget ;
例如改成下面这样:
代码语言:javascript复制class _TestShareWidgetState extends State<TestShareWidget> {
@override
Widget build(BuildContext context) {
// return Text(ShareDataWidget.of(context).data.toString());
return Text("test");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('发生改变');
}
}
复制代码
将 buid 方法中 依赖 ShareDataWidget 的代码注释掉之后,返回了一个固定的 Text,这样依赖,虽然 data 发生了变化,但是由于并没有依赖关系,所以 didChangeDependencies 方法不会被调用
因为数据发生变化时只对是该该数据的 Widget 更新是合理并且性能友好的
应该在 did.... 方法中做什么
一般来说,子 widget 会很少重新此方法,应为在依赖发生改变之后也会调用 build 方法。但是如果你需要在依赖发生改变的时候做一些昂贵的操作,如网络请求等,这时最好的方式就是在此方法中执行,这样可以避免每次在 build 的时候都执行这些昂贵的操作
深入理解 InheritedWidget 方法
如果我们只想要依赖数据,并不想在依赖变化时执行 didChangeDependencies
方法应该怎么搞,如下:
//定义一个便捷的方法,方便子树中的 widget获取共享数据
static ShareDataWidget of(BuildContext context) {
// return context.dependOnInheritedWidgetOfExactType();
return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}
复制代码
将获取 ShareDataWidget 的方式改为 context.getElementForInheritedWidgetOfExactType().widget
即可
那么这两种方式的区别是啥呢,我们看一下源码:
代码语言:javascript复制@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
return ancestor;
}
复制代码
代码语言:javascript复制@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
//相比与上面的代码,多出的部分
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
复制代码
可以看到,dependOnInheritedWidgetOfExactType 比 getElementForInheritedWidgetOfExactType 多掉了 dependOnInheritedElement
方法,他的源码如下:
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
复制代码
可以看到,dependOnInheritedElement
方法中主要是注册了依赖关系,所以,在调用 getElementForInheritedWidgetOfExactType 时,InheritedWidget 和依赖他的子孙组件便完成了注册,之后当 InheritedWidget 发生变化的时候,才可以更新依赖他的子孙组件,也就是 子孙组件的 build 和 didChangeDependencies 方法。
由于 getElementForInheritedWidgetOfExactType 没有注册依赖,所以当 InheritedElement 发生变化时,就不会更新相应子孙的 widget
注意:上例中,将 of 方法中改为 getElementForInheritedWidgetOfExactType 之后,_TestShareWidgetState 的 didChangeDependencies 方法确实不会被调用,但是build 方法还是调用了; 这是应为在点击按钮之后,会调用 _TestInheritedWidgetState的 setState 方法,此时页面会重新构建,就会导致 TestShareWidget() 也重新构建,所以他的 build 也会执行
在这种情况下,所以依赖 ShareDataWidget 的组件,只要调用了 _TestInheritedWidgetState 的 setState 方法,都会被重新执行 build,这是没必要的,那有什么办法可以避免呢,答案是使用缓存;
一个简单的做法就是通过封装一个 StatefulWidget ,将 Widget 树缓存起来,这样就可以放在 build 被执行;
跨组件状态共享 Provider
Flutter 中,状态管理一般的原则是:
- 如果组件是私有的,则组件自己管理状态
- 如果要跨组件共享,则状态由共同的父组件来管理
对于跨组件共享状态,管理的方式有很多中,如使用全局的实践总线 EventBus,他是一个观察者模式的实现,通过它就可以实现跨组件的状态同步:状态持有方:进行状态更新,发布状态和使用的;状态使用方(观察者) ,监听状态的改变事件来完成一些操作;
但是,通过观察者模式来实现跨组件有一些明显的缺点:
- 必须显式的定义各种事件,不方便管理
- 订阅者必须显式的注册状态改变回调,也必须在组件销毁的时候手动解绑回调,以避免内存泄漏
那有没有更好的管理方式呢,答案是肯定的,就是使用 InheritedWidget
,他的天生特性就是能绑定 InheritedWidget 与依赖他的子孙组件的依赖关系,并且当数据发生变化时,可以自动依赖子孙组件!,利用这个特性,我们可以将需要跨组件的状态保存在 InheritedWidget
中,然后在子组件中引用 InheritedWidget 即可。Flutter 社区著名的 Provider 包正是基于这个思想实现的一套跨组件状态共享的解决方案,下面我们便详细看一下 Provider 的用法和原理。
Provider
我们根据上面学习的 InheritedWidget 实现的思路来一步一步的实现一个最小功能的 Provider
定义一个需要保存数据的 InheritedWidget
代码语言:javascript复制///一个通用的 InheritedProvider,保存任何需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
///共享的状态使用泛型
final T data;
InheritedProvider({@required this.data, Widget child})
: super(child: child);
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
///返回true,表示每次都会调用子孙节点的 didChangeDependencies
return true;
}
}
复制代码
由于具体业务数据类型不可预期,为了通用性,这里使用了泛型
现在保存数据的地方有了,接下来需要做的就是在数据变化时重新构建 InheritedProvider,那么现在面临两个问题:
- 数据发生变化如何通知?
- 谁来重新构建
InheritedProvider
?
第一个问题其实很好解决,我们可以使用 EventBus 来进行通知,但是为了更贴近 Flutter 开发,我们使用 Flutter SDK 中提供的 ChangeNotifier
类,他继承自 Listenable
,也实现了一个 Flutter 风格的订阅者模式,定义大致如下:
class ChangeNotifier implements Listenable {
List listeners=[];
@override
void addListener(VoidCallback listener) {
//添加监听器
listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
//移除监听器
listeners.remove(listener);
}
void notifyListeners() {
//通知所有监听器,触发监听器回调
listeners.forEach((item)=>item());
}
... //省略无关代码
}
复制代码
我们可以使用 add ,remove 来添加,移除监听器,通过 notifyListeners 可以触发所有监听器的回调
接着我们将需要共享的状态放在一个 Model 类中,然后继承自 ChangeNotifier
,这样当共享的状态改变时,我们只需要调用 notifyListeners 来通知订阅者,然后订阅者重新构建 InheritedProvider,这也是第二个问题的答案,订阅类的实现如下:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
final Widget child;
final T data;
ChangeNotifierProvider({Key key, this.child, this.data});
@override
_ChangeNotifierProviderState<T> createState() =>
_ChangeNotifierProviderState<T>();
///定义一个便捷的方法,方便子树中的 widget 获取共享数据
static T of<T>(BuildContext context) {
final provider =
context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
return provider.data;
}
}
class _ChangeNotifierProviderState<T extends ChangeNotifier>
extends State<ChangeNotifierProvider<T>> {
void update() {
setState(() {});
}
@override
void initState() {
//给 model 添加监听器
widget.data.addListener(update);
super.initState();
}
@override
void dispose() {
//移除 model 的监听器
super.dispose();
}
@override
void didUpdateWidget(covariant ChangeNotifierProvider<T> oldWidget) {
//当 Provider 更新时,如果旧数据不 ==,则解绑旧数据的监听,同时添加新数据的监听
if (widget.data != oldWidget.data) {
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return InheritedProvider<T>(
data: widget.data,
child: widget.child,
);
}
}
复制代码
ChangeNotifierProvider 继承自 StatefulWidget,定义了一个 of 静态方法供子类方便获取 Widget 树的 InheritedProvider 中保存的共享状态
_ChangeNotifierProviderState 类的主要作用就是监听共享状态改变时重新构建 Widget 树。注意,在这个类中调用 setState() 方法,widget.child 始终是同一个,InheriedProvider 的 child 引用的始终是同一个子 Widget,所以 widget.child 并不会重新 build,这也就是相当于对 child 进行了缓存。当然如果 ChangeNotifierProvider 腹肌 Widget 重新 build 时,传入的 child 便有可能发生变化
现在我们需要的工具类都已经完成,下面通过根据一个例子看看如何使用上面的类
购物车示例
代码语言:javascript复制///Item类,用于表示商品的信息
class Item {
final price;
int count;
Item(this.price, this.count);
}
class CarMode extends ChangeNotifier {
//用户保存购物车中商品列表
final List<Item> _items = [];
//禁止修改购物车里的商品信息
UnmodifiableListView get items => UnmodifiableListView(_items);
//购物车商品的总价
double get totalPrice =>
_items.fold(0, (value, item) => value item.count * item.price);
void add(Item item) {
_items.add(item);
//通知监听器(观察者),重新构建 InheritedProvider,更新状态
notifyListeners();
}
}
class ProviderTest extends StatefulWidget {
@override
_ProviderTestState createState() => _ProviderTestState();
}
class _ProviderTestState extends State<ProviderTest> {
@override
Widget build(BuildContext context) {
return Center(
child: ChangeNotifierProvider(
data: CarMode(),
child: Builder(
builder: (context) {
return Column(
children: [
Builder(builder: (context) {
var cart = ChangeNotifierProvider.of<CarMode>(context);
return Text("总价 :${cart.totalPrice}");
}),
Builder(
builder: (context) {
return RaisedButton(
child: Text("添加商品"),
onPressed: () {
ChangeNotifierProvider.of<CarMode>(context)
.add(Item(20, 1));
},
);
},
)
],
);
},
),
),
);
}
}
复制代码
Item类:用于保存商品的信息系
CartMode 类:保存购物车内上面数据的类,即跨组件需要共享的 model 类
ProviderTest:最终构建的页面
每次点击添加商品,总价就会增加 20,虽然这个例子比较简单,只更新了同一个路由页中的一个状态,但是如果是一个真正的购物车,他的购物车数据通常会在 app 内共享,例如跨路由共享,将 ChangeNotifierProvider 放在整个应用的 Widget 树的根上,那么整个 app 就可以共享购物车的数据了
Provider 的原理图如下:
Model 变化后会自动通知 ChangeNotifierProvider
(订阅者),ChangeNotifierProvider
内部会重新构建 InheritedWidget
,而依赖该 InheritedWidget
的子孙 Widget 就会更新
我们可以发现使用 Provider,将会带来如下好处:
1,我们的业务代码更加的关注数据,只需要更新 Model,则 UI 会自动更新,而不用在状态改变后在去手动调用 setState 来显式的更新页面
2,数据改变的消息传递被屏蔽了,我们无需手动去处理改变事件的发布和订阅了,这一切都被封装在 Provider 中了,这帮我们省掉了大量的工作
3,在大型复杂的应用中,尤其是需要全局共享的状态非常多的时候,使用 Provider 将会大大简化我们的代码逻辑,降低出错概率,提高开发效率
优化
上面实现的 ChangeNotifierProvider 是有两个明显的缺点:代码组织问题和性能问题,下面我们来看一下
代码组织问题
代码语言:javascript复制Builder(builder: (context) {
var cart = ChangeNotifierProvider.of<CarMode>(context);
return Text("总价 :${cart.totalPrice}");
}),
复制代码
这段代码有两点可以优化
1,需要显示的调用 ChangenotifierProvider ,当 APP 内部依赖 CartMode 很多时,这样的代码就会很沉余
2,语义不明确,由于 ChangenotifierProvider 是订阅者,依赖 CarMode 的 Widget 自然就是订阅者,其实也就是状态的消费者;如果使用 Builder 来构建,语义就不是很明确,如果能使用一个更具有明确语义的 Widget,如 Consumer
,这样最终代码的语言就很明确,只要看到 Consumer
,我们就知道他是某个跨组件或者全局的状态。
为了优化这个问题,我们可以封装一个 Consumer
Widget,如下:
class Consumer<T> extends StatelessWidget {
final Widget child;
final Widget Function(BuildContext context, T value) builder;
Consumer({Key key, @required this.builder, this.child});
@override
Widget build(BuildContext context) {
return builder(context, ChangeNotifierProvider.of<T>(context)); //自动获取 model
}
}
复制代码
Cusumer 实现非常简单,它通过指定模板参数,然后内部自动调用 ChangeNotifierProvider.of 获取相应的 Mode,并且 Consumer 这个名字本身也是具有确切语义(消费者),现在上面的代码可以优化成下面这样:
代码语言:javascript复制Consumer<CarMode>(
builder: (context, cart) => Text("总价:${cart.totalPrice}"),
),
复制代码
是不是很优雅呢?
性能问题
上面代码还有一个性能问题,在 添加按钮的地方:
代码语言:javascript复制Builder(
builder: (context) {
return RaisedButton(
child: Text("添加商品"),
onPressed: () {
ChangeNotifierProvider.of<CarMode>(context)
.add(Item(20, 1));
},
);
},
)
复制代码
点击添加商品按钮后,由于购物车总价会发生变化,所以显示总结的 Text 是符合预期的。
但是 添加商品 按钮本身没有啥变化,所以他是不应该被重新 build 的,但是运行发现,每次点击的时候按钮都会被重新build。这是为什么呢,这是因为 RadisedButton 的 build 中调用了 ChangeNotifierProvider.of() ,也就是说依赖了 Widget树上面的 InheritedWidget (即 InheritedProvider) Widget ,
所以当添加完商品后,CartMode 发生了变化,会通知子树,以 InheritedProvider 将会更新,此时依赖他的子孙 Wdiget 都会被重新构建。
问题找到了,那么如何避免这个不必要的重构呢?
既然问题是 按钮和 InheritedWidget 建立了依赖联系,那么我们只要打破这种依赖关系就好了,如何打破呢,在最上面的时候就讲过了:调用 dpendOnInheritedWidgetOfExactType
和 getElementForInheritedWidgetOfExactType
的区别就是前者会注册依赖关系,而后者则不会,所有只需要将 ChangeNotiferProvider.of 的实现改为下面这样即可:
///listen:是否建立以来关系
static T of<T>(BuildContext context, {bool listen = true}) {
final provider = listen
? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()
: context
.getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()
?.widget as InheritedProvider<T>;
return provider.data;
}
复制代码
修改后再次运行,就会发现按钮不会重新构建了,而总价任然后更新。
至此,我们实现了一个迷你版的 Provider,它具备 Pub 上 Provider package 的核心功能,但是由于我们的功能并不全面,只实现了一个可监听的 ChangeNotiferProvider,并没有实现数据共享,另外,我们的实现有些边界值没有考虑到,比如如何保证在 Widget 树重新 build 时 Mode 始终是单例等等。所以建议项目中还是使用 Provider Package,这篇文章只是帮助大家了解 Provider Package 的底层原理
本文参考 Fluuter实战 书籍