1.简介
InheritedWidget是Flutter提供的一个非常重要的功能性组件,可以实现在widget树中从上到下数据共享数据,比如我们的widget树中有一个InheritedWidget,并且共享了一个数据,那么我们在InheritedWidge的任意子widget就可以获取到轻松InheritedWidget的共享数据,比如Theme和Local正是通过InheritedWidget实现在widget中共享数据的,再比如大名鼎鼎的Provider其实正是通过InheritedWidget实现数据共享和状态管理的
2.例子
我们现在通过一个计数器的例子演示一下InheritedWidget是如何实现数据共享的
首先写一个类继承自InheritedWidget,代码如下:
class ShareWidget extends InheritedWidget {
final int count;
const ShareWidget({super.key, required super.child, required this.count});
/// 定义一个方法,方便子widget获取共享数据
static ShareWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareWidget>();
}
@override
bool updateShouldNotify(covariant ShareWidget oldWidget) {
/// 表示当数据发生改变的时候通知依赖InheritedWidget数据的子widget重新build
return oldWidget.count != count;·
}
}
再定义一个CountWidget,并且调用ShareWidget? of(BuildContext context)方法获取共享数据,代码如下:
class CountWidget extends StatefulWidget {
CountWidget({super.key});
@override
State<StatefulWidget> createState() => _CountState();
}
class _CountState extends State<CountWidget> {
@override
Widget build(BuildContext context) {
print('CountWidget build');
final count = ShareWidget.of(context)?.count ?? 0;
return Text(count.toString());
}
}
然后在页面中使用CountWidget,代码如下:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ShareWidget(count: _counter, child: const CountWidget()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
运行工程,看到如下的界面
点击右下角增加按钮,发现中间的数据在增加,并且控制台打印CountWidget build,说明CountWidget获取共享数据成功了,并且每次获取数据的时候都会重新build一次,我们再修改一下CountWidget 的代码,将获取共享数据的代码去掉,代码如下:
class CountWidget extends StatefulWidget {
const CountWidget({super.key});
@override
State<StatefulWidget> createState() => _CountState();
}
class _CountState extends State<CountWidget> {
@override
Widget build(BuildContext context) {
print('CountWidget build');
/// final count = ShareWidget.of(context)?.count ?? 0;
return Text('0');
}
}
重新运行一下工程,发现控制台不会再打印CountWidget build,说明每次点击增加按钮的时候CountWidget 不会重新build,这就说明了当子widget依赖父InheritedWidget中的数据的时候,只要数据发生改变,子widget就会重新build,如果没有依赖共享数据,即使数据变化了,子widget也不会重新build
3.深入理解InheritedWidget
我们给_CountState 重写didChangeDependencies方法,代码如下:
class CountWidget extends StatefulWidget {
const CountWidget({super.key});
@override
State<StatefulWidget> createState() => _CountState();
}
class _CountState extends State<CountWidget> {
@override
Widget build(BuildContext context) {
print('CountWidget build');
final count = ShareWidget.of(context)?.count ?? 0;
return Text(count.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
/// build方法如果中没有依赖InheritedWidget的共享数据,此回调不会被调用。
print("didChangeDependencies");
}
}
我们会发现每次InheritedWidget的共享数据发生变化的时候,控制台都会打印didChangeDependencies,说明didChangeDependencies()被调用了,但是如果我们把CountWidget对于共享数据的依赖去掉,每次共享数据发生改变的时候控制台都不会打印didChangeDependencies,说明didChangeDependencies()没有被调用,也就是说只要子widget不依赖InheritedWidget的共享数据,即便是整个页面被setState了,CountWidget 的didChangeDependencies()也不会被调用,我们来看看didChangeDependencies的源码注释,如下:
/// Called when a dependency of this [State] object changes.
///
/// For example, if the previous call to [build] referenced an
/// [InheritedWidget] that later changed, the framework would call this
/// method to notify this object about the change.
///
/// This method is also called immediately after [initState]. It is safe to
/// call [BuildContext.dependOnInheritedWidgetOfExactType] from this method.
///
/// Subclasses rarely override this method because the framework always
/// calls [build] after a dependency changes. Some subclasses do override
/// this method because they need to do some expensive work (e.g., network
/// fetches) when their dependencies change, and that work would be too
/// expensive to do for every build.
@protected
@mustCallSuper
void didChangeDependencies() { }
这段注释最核心的注释其实就是第一句,意思就是说当这个State的对象的依赖发生改变的时候调用,这里所说的依赖其实指的是子widget对父InheritedWidget的共享数据的依赖,当共享数据发生变化的时候,这个didChangeDependencies方法就会被调用,如果我们只是想在CountWidget 使用共享数据,不希望共享数据发生变化的时候didChangeDependencies方法被调用,该如何做呢?其实很简单,只要修改ShareWidget.of(context)方法实现就可以了 , 修改代码如下就可以了
static ShareWidget? of(BuildContext context) {
// return context.dependOnInheritedWidgetOfExactType<ShareWidget>();
return context
.getElementForInheritedWidgetOfExactType<ShareWidget>()
?.widget as ShareWidget;
}
这样修改后当依赖的共享数据发生变化的时候,CountWidget 的build和 didChangeDependencies方法都不会被调用,为什么会这样呢?我们来看一下dependOnInheritedWidgetOfExactType()和getElementForInheritedWidgetOfExactType()方法的内部实现有什么区别,代码如下:
@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
//多出的部分
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
可以看到dependOnInheritedWidgetOfExactType()比getElementForInheritedWidgetOfExactType()多调了一个dependOnInheritedElement()方法,我们再看看dependOnInheritedElement()的实现,代码如下
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
我们其实可以看到InheritedWidget 和依赖它的子组件注册了依赖关系,这也就是为什么使用dependOnInheritedWidgetOfExactType()会导致子组件的build()和didChangeDependencies()在共享数据发生变化后被重新调一次的原因,了解了这一点,对我们后面开发一个简易的Provider会有帮助的,后面会说明为什么会有帮助,这里先卖个关子
4.自定义Provider
1. 自定义过程
我们将基于上面对InheritedWidget的理解来实现一个最简单的Provider,因为我们需要在组件树中共享数据,所以对于需要监听共享数据的子孙widget, 我们需要使用一个InheritedWidget 将它包起来,然后让这个子孙widget监听InheritedWidget 提供的共享数据,同时,为了代码的复用性和通用性,我们需要写一个类继承自InheritedWidget ,并且结合泛型,实现共享数据的抽象,代码如下:
class ProviderDataWidget<T> extends InheritedWidget {
const ProviderDataWidget({super.key,
required this.data,
required Widget child,
}) : super(child: child);
final T data;
@override
bool updateShouldNotify(ProviderDataWidget<T> old) {
//先简单返回true,这样一来不够数据变化前是否一致,都会调用依赖其的子孙节点的`didChangeDependencies`。
return true;
}
}
凡是需要监听共享数据的widget,我们都让它成为ProviderDataWidget的子孙widget
我们在实际开发中,widget和业务逻辑一般都是分开写在不同的类里的,一般业务逻辑我们都是写在ViewModel或者Provider类中,那么,在这些写业务逻辑的类中,我们如何在数据发生改变的时候通知widget 刷新页面呢?我们看了Provider状态管理框架的源码后发现我们可以使用ChangeNotifier,
ChangeNotifier的源码大致如下:
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());
}
... //省略无关代码
}
ChangeNotifier 实现了一种发布者-订阅者模式,我们同通过addListener方法添加监听方法,removeListener方法移除监听方法,notifyListeners方法回调所有监听方法,现在我们只需要将我们的业务逻辑放在一个Provider类中,让它继承自ChangeNotifier,成为一个发布者,每当数据有变化的时候通知widget刷新
有了发布者,我们还需要一个订阅者,订阅者其实就是widget,但是,为了代码的复用性和通用性,我们需要封装一个通用的widget,以此来完成自动订阅,自动解绑订阅,提供共享数据,代码如下:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
const ChangeNotifierProvider({super.key,
required this.data,
required this.child,
});
final Widget child;
final T data;
//定义一个便捷方法,方便子树中的widget获取共享数据
static T of<T>(BuildContext context) {
final provider = context.dependOnInheritedWidgetOfExactType<ProviderDataWidget<T>>();
return provider!.data;
}
@override
_ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();
}
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>> {
void update() {
//如果数据发生变化(provider类调用了notifyListeners),重新构建InheritedProvider
setState(() => {});
}
@override
void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
//当Provider更新时,如果新旧数据不"==",则解绑旧数据监听,同时添加新数据监听
if (widget.data != oldWidget.data) {
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
// 给provider添加监听器
widget.data.addListener(update);
super.initState();
}
@override
void dispose() {
// 移除provider的监听器
widget.data.removeListener(update);
super.dispose();
}
@override
Widget build(BuildContext context) {
print('_ChangeNotifierProviderState build');
return ProviderDataWidget<T>(
data: widget.data,
child: widget.child,
);
}
我们在_ChangeNotifierProviderState实现了对provider的监听,当监听的provider刷新监听方法的时候,这个类的setState()方法就会被调用,从而完成widget的刷新,但是呢?因为刷新的时候,widget.child始终是同一个,所以widget.child如果没有监听共享数据,widget.child是不会被刷新的
2. 使用
我们将展示用户信息,并且在用户信息改变的时候刷新widget,展示最新的用户信息
我们需要在provider中提供用户信息,并且在用户信息改变的时候刷新widget,代码如下:
class UserInfoProvider extends ChangeNotifier {
var _userInfo = UserInfo('xiaoHong', 11);
get userInfo => _userInfo;
set userInfo(value) {
_userInfo = value;
notifyListeners();
}
UserInfo getUserInfo() => _userInfo;
}
我们还需要在widget中使用ChangeNotifierProvider,监听用户数据的变化,代码如下:
class UserInfoWidget extends StatefulWidget {
const UserInfoWidget({super.key});
@override
State<StatefulWidget> createState() => UserInfoState();
}
class UserInfoState extends State<UserInfoWidget> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<UserInfoProvider>(
data: UserInfoProvider(),
child: Column(
children: [
Builder(builder: (context) {
print('user text build');
final userInfo =
ChangeNotifierProvider.of<UserInfoProvider>(context).userInfo;
return Text('${userInfo.name}, ${userInfo.age}岁');
}),
const SizedBox(
height: 30,
),
Builder(builder: (context) {
print('change text build');
return GestureDetector(
onTap: () {
ChangeNotifierProvider.of<UserInfoProvider>(context)
.userInfo = UserInfo('xiaoNan', 17);
},
child: const Text('change'),
);
})
],
));
}
}
运行代码后出现界面如下
我们点击一下 change Text,修改用户信息,发现界面会被刷新展示正确的新的用户信息,说明我们初步的provider已经完成,可以让widget监听数据的变化,实现状态共享,而且可以使widget和业务逻辑相分离
3. 改进
上面的做法其实是存在一些性能问题的,每次点击 change widget的时候控制台都会打印‘change text build’,这说明change Text被rebuild了,这是不对的,因为change Text并没有监听共享数据,不应该在数据变化的时候刷新,那么这个问题该如何解决了,其实很简单,还记得在上面第三点深入理解InheritedWidget卖了个关子吗?我们只需要修改ChangeNotifierProvider.of方法的实现即可,代码如下:
//添加一个listen参数,表示是否建立依赖关系
static T of<T>(BuildContext context, {bool listen = true}) {
final provider = listen
? context.dependOnInheritedWidgetOfExactType<ProviderDataWidget<T>>()
: context.getElementForInheritedWidgetOfExactType<ProviderDataWidget<T>>()?.widget
as ProviderDataWidget<T>;
return provider!.data;
}
我们添加了一个listen参数,表示是否需要让子孙widget和InheritedWidget建立依赖关系,如果listen为false,表示不需要建立依赖,那么在共享数据发生变化的时候就不会刷新子孙widget
我们再修改一下change widget的代码,如下所示:
Builder(builder: (context) {
print('change text build');
return GestureDetector(
onTap: () {
ChangeNotifierProvider.of<UserInfoProvider>(context,listen: false)
.userInfo = UserInfo('xiaoNan', 17);
},
child: const Text('change'),
);
})
然后我们再点击change widget,发现控制台不会再change text build,说明change widget没有被rebuild,性能问题也就被解决了