拦截返回键(WillPopScope)
为了避免用户误触返回按钮而导致 App 退出,在很多 App 中都拦截了用户点击返回键的按钮,然后进行一些防误触判断,比如当用户在某一个时间段内点击两次时,才会认为用户是要退出(而非误触)。
Flutter中可以通过WillPopScope
来实现返回按钮拦截,我们看看WillPopScope
的默认构造函数:
const WillPopScope({
...
required WillPopCallback onWillPop,
required Widget child
})
onWillPop
是一个回调函数,当用户点击返回按钮时被调用(包括导航返回按钮及Android物理返回按钮)。该回调需要返回一个Future
对象,如果返回的Future
最终值为false
时,则当前路由不出栈(不会返回);最终值为true
时,当前路由出栈退出。我们需要提供这个回调来决定是否退出。
示例:为了防止用户误触返回键退出,我们拦截返回事件。当用户在1秒内点击两次返回按钮时,则退出;如果间隔超过1秒则不退出,并重新记时。
代码如下:
class WillPopScopeTestRoute extends StatefulWidget {
const WillPopScopeTestRoute({
Key? key}) : super(key: key);
WillPopScopeTestRouteState createState() {
return WillPopScopeTestRouteState();
}
}
class WillPopScopeTestRouteState extends State<WillPopScopeTestRoute> {
DateTime? _lastPressedAt; //上次点击时间
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt!) > const Duration(seconds: 1)) {
// 两次点击间隔超过1秒则重新计时
_lastPressedAt = DateTime.now();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text("再按一次退出页面"),
action: SnackBarAction(label: "确定", onPressed: () => {
},),
duration: const Duration(milliseconds: 1000),
),
);
return false;
}
return true;
},
child: Container(
alignment: Alignment.center,
child: const Text("1秒内连续按两次返回键退出"),
)
);
}
}
数据共享(InheritedWidget)
InheritedWidget
是 Flutter 中非常重要的一个功能型组件,它提供了一种在 widget
树中从上到下共享数据的方式,比如我们在应用的根 widget
中通过InheritedWidget
共享了一个数据,那么我们便可以在任意子widget
中来获取该共享的数据!这个特性在一些需要在整个 widget
树中共享数据的场景中非常方便!如 Flutter SDK 中正是通过 InheritedWidget
来共享应用主题Theme
和 Locale
(当前语言环境)信息的。
InheritedWidget
和 React 中的context
功能类似,和逐级传递数据相比,它们能实现组件跨级传递数据。InheritedWidget
的在widget
树中数据传递方向是从上到下的,这和通知Notification
的传递方向正好相反。
下面我们看一下“计数器”示例应用程序的InheritedWidget
版本。需要说明的是,本示例主要是为了演示InheritedWidget
的功能特性,并不是计数器的推荐实现方式。
首先,我们通过继承InheritedWidget
,将当前计数器点击次数保存在ShareDataWidget
的data
属性中:
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({
Key? key, required this.data, required Widget child,}) : super(key: key, child: child);
final int data; // 需要在子树中共享的数据,保存点击次数
// 定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
}
// 该回调决定当data发生变化时,是否通知子树中依赖data的Widget重新build
bool updateShouldNotify(ShareDataWidget old) {
return old.data != data;
}
}
然后我们实现一个子组件_TestWidget
,在其build
方法中引用ShareDataWidget
中的数据。同时,在其didChangeDependencies()
回调中打印日志:
class _TestWidget extends StatefulWidget {
_TestWidgetState createState() => _TestWidgetState();
}
class _TestWidgetState extends State<_TestWidget> {
Widget build(BuildContext context) {
// 使用InheritedWidget中的共享数据
return Text(ShareDataWidget.of(context)!.data.toString());
}
void didChangeDependencies() {
super.didChangeDependencies();
// 父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。
// 如果build中没有依赖InheritedWidget,则此回调不会被调用。
print("Dependencies change");
}
}
didChangeDependencies 回调:
-
在之前介绍
StatefulWidget
的生命周期时,我们提到State
对象有一个didChangeDependencies
回调,它会在“依赖”发生变化时被 Flutter 框架调用。而这个“依赖”指的就是子widget
是否使用了父widget
中InheritedWidget
的数据!如果使用了,则代表子widget
有依赖;如果没有使用则代表没有依赖。 -
这种机制可以使子组件在所依赖的
InheritedWidget
变化时来更新自身!比如当主题、locale(语言)等发生变化时,依赖其的子widget
的didChangeDependencies
方法将会被调用。
最后,我们创建一个按钮,每点击一次,就将ShareDataWidget
的值自增:
class InheritedWidgetTestRoute extends StatefulWidget {
const InheritedWidgetTestRoute({
Key? key}) : super(key: key);
State createState() => _InheritedWidgetTestRouteState();
}
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
int count = 0;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("ShareDataWidget"),
),
body: Center(
child: ShareDataWidget(
// 使用ShareDataWidget
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: _TestWidget(), // _TestWidget中依赖ShareDataWidget
),
ElevatedButton(
child: const Text("Increment"),
// 每点击一次,将count自增,然后重新build, ShareDataWidget的data将被更新
onPressed: () => setState(() => ++count),
),
],
),
),
),
);
}
}
每点击一次按钮,计数器就会自增,控制台就会打印一句日志:
I/flutter ( 8513): Dependencies change
可见依赖发生变化后,其didChangeDependencies()
会被调用。但是需要注意,如果_TestWidget
的build
方法中没有使用ShareDataWidget
的数据,那么它的didChangeDependencies()
将不会被调用,因为它并没有依赖ShareDataWidget
。
例如,我们将_TestWidgetState
代码改为下面这样,didChangeDependencies()
将不会被调用:
class _TestWidgetState extends State<_TestWidget> {
Widget build(BuildContext context) {
return Text("text");
}
void didChangeDependencies() {
super.didChangeDependencies();
// build方法中没有依赖InheritedWidget,此回调不会被调用。
print("Dependencies change");
}
}
上面的代码中,我们将build()
方法中依赖ShareDataWidget
的代码去掉了,然后返回一个固定Text
,这样一来,当点击Increment
按钮后,ShareDataWidget
的data
虽然发生变化,但由于_TestWidgetState
并未依赖ShareDataWidget
,所以_TestWidgetState
的didChangeDependencies
方法不会被调用。其实,这个机制很好理解,因为在数据发生变化时只对使用该数据的Widget更新是合理并且性能友好的。
应该在didChangeDependencies()中做什么?
一般来说,子 widget
很少会重写此方法,因为在依赖改变后 Flutter 框架也都会调用build()
方法重新构建组件树。但是,如果你需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行,这样可以避免每次build()
都执行这些昂贵操作。
深入了解InheritedWidget
现在来思考一下,在上面的例子中,如果我们只想在_TestWidgetState
中引用ShareDataWidget
数据,但却不希望在ShareDataWidget
发生变化时调用_TestWidgetState
的didChangeDependencies()
方法应该怎么办?其实答案很简单,我们只需要将ShareDataWidget.of()
的实现改一下即可:
// 定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget of(BuildContext context) {
//return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}
唯一的改动就是获取ShareDataWidget
对象的方式,把dependOnInheritedWidgetOfExactType()
方法换成了context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget
,那么他们到底有什么区别呢,我们看一下这两个方法的源码(实现代码在Element
类中):
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
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
源码如下:
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {
Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
可以看到 在dependOnInheritedElement
方法中主要是注册了依赖关系! 看到这里也就清晰了,调用dependOnInheritedWidgetOfExactType()
和 getElementForInheritedWidgetOfExactType()
的区别就是前者会注册依赖关系,而后者不会。
所以在调用dependOnInheritedWidgetOfExactType()
时,InheritedWidget
和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget
发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的didChangeDependencies()
方法和build()
方法。
而当调用的是 getElementForInheritedWidgetOfExactType()
时,由于没有注册依赖关系,所以之后当InheritedWidget
发生变化时,就不会更新相应的子孙Widget
。
注意,如果将上面示例中ShareDataWidget.of()
方法实现改成调用getElementForInheritedWidgetOfExactType()
后,点击"Increment
"按钮,会发现虽然_TestWidgetState
的didChangeDependencies()
方法确实不会再被调用,但是其build()
仍然会被调用!造成这个的原因其实是,点击"Increment
"按钮后,会调用_InheritedWidgetTestRouteState
的setState()
方法,此时会重新构建整个页面,由于示例中,_TestWidget
并没有任何缓存,所以它也都会被重新构建,所以也会调用build()
方法。
那么,现在就带来了一个问题:实际上,我们只想更新子树中依赖了ShareDataWidget
的组件,而现在只要调用_InheritedWidgetTestRouteState
的setState()
方法,所有子节点都会被重新build
,这很没必要,那么有什么办法可以避免呢?答案是缓存!一个简单的做法就是通过封装一个StatefulWidget
,将子Widget
树缓存起来(具体做法后面会介绍如何通过 Provider
Widget 来实现)。
InheritedWidget 源码分析
一般来说,dependOnInheritedWidgetOfExactType
方法是子节点向祖先节点获取数据的入口,所以也是分析的切入点,其逻辑如代码清单8-9所示。
// 代码清单8-9 flutter/packages/flutter/lib/src/widgets/framework.dart
// Element
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({
Object? aspect}) {
// 从_inheritedWidgets中获取指定Widget类型的InheritedElement,生成逻辑见代码清单8-14
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {
Object? aspect }) {
_dependencies ??= HashSet<InheritedElement>(); // 记录自身所依赖的InheritedElement节点
_dependencies!.add(ancestor); // 新增一个依赖
ancestor.updateDependencies(this, aspect); // 告知被依赖节点当前节点请求依赖,见代码清单8-10
return ancestor.widget; // 返回T类型的Widget节点
}
以上逻辑首先会通过_inheritedWidgets
从Element Tree
中获取距离最近的T
类型的InheritedElement
节点。至于为什么是最近,将在后面内容分析。得到的节点ancestor
就是当前Element
节点所要依赖的节点。dependOnInheritedElement
方法的主要逻辑是取出当前节点的_dependencies
字段,其包含了自身所依赖的全部InheritedElement
节点,此时将添加ancestor
对象,然后调用ancestor
的updateDependencies
方法,如代码清单8-10所示。
// 代码清单8-10 flutter/packages/flutter/lib/src/widgets/framework.dart
class InheritedElement extends ProxyElement {
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
// dependent 即代码清单8-9中调用本方法的对象
void updateDependencies(Element dependent, Object? aspect) {
setDependencies(dependent, null);
}
void setDependencies(Element dependent, Object? value) {
// 通过_dependents记录了所有依赖自身的dependent节点
// 以便自身数据更新时能通知到该节点,详见代码清单8-12
_dependents[dependent] = value;
}
}
因为以上逻辑主要是将当前节点加入ancestor
的_dependents
字段,所以依赖节点和被依赖节点都互相记录了对方,如图8-2所示。
那么,基于这种数据结构,ancestor
如何在自身数据改变时触发对应的回调呢?首先分析InheritedElement
的update
方法,它是因数据改变而开始更新自身的入口,如代码清单8-11所示。
// 代码清单8-11 flutter/packages/flutter/lib/src/widgets/framework.dart
abstract class ProxyElement extends ComponentElement {
void update(ProxyWidget newWidget) {
// 在Build流程中触发
final ProxyWidget oldWidget = widget as ProxyWidget; // 记录旧的Widget配置
super.update(newWidget);
updated(oldWidget);
rebuild(force: true);
}
void rebuild({
bool force = false}) {
...
try {
performRebuild();
} finally {
...
}
...
}
void performRebuild() {
_dirty = false; // 标记为需要重新进行Build流程
}
void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget); // 见代码清单8-12
}
Widget build() => widget.child; // 即被代理的Widget,该Widget在InheritedWidget初始化时传入
}
class InheritedElement extends ProxyElement {
// updated方法是ProxyElement特有的,注意与update方法区分
void updated(InheritedWidget oldWidget) {
// updateShouldNotify是为InheritedWidget的子类提供一个控制依赖更新条件的入口
if ((widget as InheritedWidget).updateShouldNotify(oldWidget)) {
super.updated(oldWidget);
}
}
}
以上逻辑中,首先调用updated
方法,该方法通过notifyClients
触发didChangeDependencies
方法。rebuild
方法最终将调用自身的build
方法,可以发现,ProxyElement
的build
方法直接返回了其子Widget
,因为它的角色本身就是代理,具体的Build流程逻辑在被代理的Widget
中。此外,InheritedWidget
的构造函数由const
修饰,其对应的Element Tree
的子树会在下一轮Build流程中直接保留。
那么真正受影响的子节点又是如何刷新的呢?首先分析notifyClients
方法,如代码清单8-12所示。
// 代码清单8-12 flutter/packages/flutter/lib/src/widgets/framework.dart
class InheritedElement extends ProxyElement {
void notifyClients(InheritedWidget oldWidget) {
// 这里的_dependents.keys记录了依赖它的所有Element节点
for (final Element dependent in _dependents.keys) {
// 注册逻辑见代码清单8-10
notifyDependent(oldWidget, dependent);
}
}
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies(); // 触发依赖节点的回调,见代码清单8-13
}
}
以上逻辑主要是遍历_dependents
字段的所有key
,即所有依赖当前节点的Element
对象,并调用其didChangeDependencies
方法,如代码清单8-13所示。
// 代码清单8-13 flutter/packages/flutter/lib/src/widgets/framework.dart
class StatefulElement extends ComponentElement {
void didChangeDependencies() {
super.didChangeDependencies(); // 第1步,Element的逻辑,触发Build流程
_didChangeDependencies = true; // 标记当前节点依赖改变,对应代码清单8-3中的判断
}
}
abstract class Element extends DiagnosticableTree implements BuildContext {
void didChangeDependencies() {
markNeedsBuild(); // 标记当前节点需要更新
}
void markNeedsBuild() {
if (_lifecycleState != _ElementLifecycle.active) return; // 状态异常
if (dirty) return; // 已经标记
_dirty = true; // 标记为需要重新进行Build流程
owner!.scheduleBuildFor(this); // 见代码清单5-45
}
}
以上逻辑,第1
步通过Element
的markNeedsBuild
方法将依赖的节点标记为dirty
,并请求一帧的更新。然后,将当前Element
节点的_didChangeDependencies
字段标记为true
,由代码清单8-3可知,对于StatefulElement
,将依次触发其didChangeDependencies
和build
方法回调。
// 代码清单8-3 flutter/packages/flutter/lib/src/widgets/framework.dart
// StatefulElement
void performRebuild() {
if (_didChangeDependencies) {
// 通常在代码清单8-13中设置为true,详见8.2节
state.didChangeDependencies(); // 当该字段为true时触发didChangeDependencies回调
_didChangeDependencies = false;
}
super.performRebuild(); // 父类该方法中会调用build()方法
}
Widget build() => state.build(this); // 由上面super.performRebuild()触发
以上便是InheritedWidget
的巧妙之处,通过两个字段成功实现了Element Tree
的局部刷新,以图8-2为例,Element A
数据改变时,其子树不会完全重新构建,只有Element B
及其子树会重新构建。
最后,分析一下代码清单8-9中_inheritedWidgets
是如何生成的。Element Tree
新挂载一个节点时,将触发_updateInheritance
方法,如代码清单8-14所示。
// 代码清单8-14 flutter/packages/flutter/lib/src/widgets/framework.dart
class InheritedElement extends ProxyElement {
void _updateInheritance() {
// 见代码清单5-3
final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null) // 继承父节点的可用依赖,即InheritedWidget的子类集合
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else // 新建一个空的集合
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets![widget.runtimeType] = this; // 记录当前节点,注意该操作会覆盖类型相同的节点
}
}
abstract class Element extends DiagnosticableTree implements BuildContext {
void _updateInheritance() {
// InheritedElement重写该方法并添加自身作为一个可用依赖
_inheritedWidgets = _parent?._inheritedWidgets; // 默认逻辑,继承父类的可用依赖
}
}
以上逻辑其实十分清晰:每个InheritedElement
会以自身对应的Widget
的类型为Key
,将自身加入_inheritedWidgets
集合,而对于其他类型的Element
则直接继承父节点的_inheritedWidgets
信息。因此,仅当B Widget
是A Widget
的子节点时,才能通过InheritedWidget
的方式完成局部刷新。
至于销毁逻辑,将在Element
节点被移除出Element Tree
时,触发在Element
的deactivate()
方法,该方法中会将当前Element
节点从其所依赖的所有父节点的Map
数据结构中移除:
// 代码清单8-7 flutter/packages/flutter/lib/src/widgets/framework.dart
void deactivate() {
// Element
if (_dependencies != null && _dependencies!.isNotEmpty) {
// 依赖清理
for (final InheritedElement dependency in _dependencies!)
dependency._dependents.remove(this);
}
_inheritedWidgets = null;
_lifecycleState = _ElementLifecycle.inactive; // 更新状态
}
以上便是InheritedWidget
的全部奥秘。
总结:
通过dependOnInheritedWidgetOfExactType
方法,子节点和父节点相互记录了对方,数据变化时父节点通过观察者模式通知所有的依赖它的子节点进行更新。
-
对于被依赖的父节点,通过
_dependents
这个Map
字段的key
记录了所有的对其依赖的子节点。 -
对于依赖
InheritedWidget
的子节点,通过_inheritedWidgets
这个Map
字段以key-value
的形式存储当前Widget
类型对应的Element
对象,或者直接从其父节点继承(如果父节点有可用的依赖信息) -
当需要更新时,会在Build流程中触发
InheritedElement
的update
方法,该方法最终调用逻辑会遍历_dependents
这个Map
的每个key
,即拿到所有依赖其的子节点Element
对象,然后调用每一个子节点Element
的didChangeDependencies
方法。这将触发
StatefulElement
的markNeedsBuild
方法将节点标记为dirty
,并请求一帧的更新,最终将触发StatefulWidget
对应State
类对象(StatefulElement
持有)的didChangeDependencies
方法和build
方法执行。
跨组件状态共享
通过事件同步状态
在 Flutter 开发中,状态管理是一个永恒的话题。一般的原则是:如果状态是组件私有的,则应该由组件自己管理;如果状态要跨组件共享,则该状态应该由各个组件共同的父元素来管理。对于组件私有的状态管理很好理解,但对于跨组件共享的状态,管理的方式就比较多了,如使用全局事件总线event_bus,它是一个观察者模式的实现,通过它就可以实现跨组件状态同步:状态持有方(发布者)负责更新、发布状态,状态使用方(观察者)监听状态改变事件来执行一些操作。下面我们看一个登录状态同步的简单示例:
定义事件:
enum Event{
login,
... //省略其他事件
}
登录页代码大致如下:
// 登录状态改变后发布状态改变事件
bus.emit(Event.login);
依赖登录状态的页面:
void onLoginChanged(e){
//登录状态变化处理逻辑
}
void initState() {
//订阅登录状态改变事件
bus.on(Event.login,onLogin);
super.initState();
}
void dispose() {
//取消订阅
bus.off(Event.login,onLogin);
super.dispose();
}
我们可以发现,通过观察者模式来实现跨组件状态共享有一些明显的缺点:
- 必须显式定义各种事件,不好管理。
- 订阅者必须需显式注册状态改变回调,也必须在组件销毁时手动去解绑回调以避免内存泄露。
在Flutter当中有没有更好的跨组件状态管理方式了呢?答案是肯定的,那怎么做的?我们想想前面介绍的InheritedWidget
,它的天生特性就是能绑定InheritedWidget
与依赖它的子孙组件的依赖关系,并且当InheritedWidget
数据发生变化时,可以自动更新依赖的子孙组件!利用这个特性,我们可以将需要跨组件共享的状态保存在InheritedWidget
中,然后在子组件中引用InheritedWidget
即可,Flutter社区著名的provider包正是基于这个思想实现的一套跨组件状态共享解决方案,接下来我们便详细介绍一下Provider
的用法及原理。
Provider
provider是Flutter官方出的状态管理包,为了加强读者对其原理的理解,我们不直接去看Provider包的源代码,相反,通过InheritedWidget
实现的思路来一步一步地实现一个最小功能的Provider。
自定义实现迷你版 Provider
首先,我们需要一个能够保存共享数据的InheritedWidget
,由于具体业务数据类型不可预期,为了通用性,我们使用泛型,定义一个通用的InheritedProvider
类,它继承自InheritedWidget
:
// 一个通用的InheritedWidget,保存需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
InheritedProvider({
required this.data, required Widget child}) : super(child: child);
final T data;
bool updateShouldNotify(InheritedProvider<T> old) {
//在此简单返回true,则每次更新都会调用依赖其的子孙节点的`didChangeDependencies`。
return true;
}
}
数据保存的地方有了,那么接下来我们需要做的就是在数据发生变化的时候来重新构建InheritedProvider
,那么现在就面临两个问题:
- 数据发生变化怎么通知?
- 谁来重新构建
InheritedProvider
?
第一个问题其实很好解决,我们当然可以使用之前介绍的eventBus
来进行事件通知,但是为了更贴近Flutter开发,我们使用 Flutter SDK 中提供的ChangeNotifier
类 ,它继承自Listenable
,也实现了一个Flutter风格的发布者-订阅者模式,ChangeNotifier
定义大致如下:
class ChangeNotifier implements Listenable {
List listeners = [];
void addListener(VoidCallback listener) {
listeners.add(listener); // 添加监听器
}