注:本文代码基于Flutter SDK 3.13.5
一、前言
在之前解读Flutter源码之InheritedWidget一文中,我们已知晓,在Widget多层嵌套的情况下,将数据从父级Widget传递给子级Widget时,采用InheritedWidget的方式非常好用。
但是,如果传递方向是反过来的情况下,也就是变为从子级Widget向父级Widget传递数据,那么依然可以选择从InheritedWidget传入Listener的方式来实现。
例如,还是之前的计数器示例,只不过添加多一个“Post”按钮来触发子级Widget向父级Widget传递数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyPage(title: '计数器'),
);
}
}
class MyPage extends StatefulWidget {
const MyPage({super.key, required this.title});
final String title;
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: MyParent(
counter: _counter,
listener: (String result) => ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(result))),
child: const MyChild(),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
tooltip: 'increment',
child: const Icon(Icons.add),
),
);
}
}
class MyParent extends InheritedWidget {
final int counter;
final ValueChanged<String> listener;
const MyParent({
super.key,
required this.counter,
required this.listener,
required super.child,
});
static MyParent? maybeOf(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<MyParent>();
static MyParent of(BuildContext context) {
final MyParent? result = maybeOf(context);
assert(result != null, 'No MyParent found in context');
return result!;
}
@override
bool updateShouldNotify(covariant MyParent oldWidget) => counter != oldWidget.counter;
}
class MyChild extends StatefulWidget {
const MyChild({super.key});
@override
State<MyChild> createState() => _MyChildState();
}
class _MyChildState extends State<MyChild> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint('didChangeDependencies');
}
@override
Widget build(BuildContext context) {
debugPrint('build');
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${MyParent.of(context).counter}',
style: const TextStyle(
fontSize: 100,
color: Colors.black,
),
),
ElevatedButton(
onPressed: () => MyParent.of(context).listener('Hello World!'),
child: const Text('Post'),
),
],
),
);
}
}
|
程序运行起来后,效果如下:
另外,Flutter的NotificationListener(通知监听)为我们提供了一个更优雅的方式去实现上面的效果,并且功能更强大,可以传递给多个父级Widget处理。
二、什么是通知监听?
关于NotificationListener(通知监听),在Flutter官网中是找不到它的详细介绍的,而且NotificationListener的代码注释也非常少,先来看下它的声明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
/// 一个监听在树上冒泡的Notification的widget
/// A widget that listens for [Notification]s bubbling up the tree.
///
/// 仅当Notifications的runtimeType是T的子类型时,才会触发onNotification回调。
/// Notifications will trigger the [onNotification] callback only if their
/// [runtimeType] is a subtype of `T`.
///
/// 要发送通知,请使用Notification.dispatch方法。
/// To dispatch notifications, use the [Notification.dispatch] method.
class NotificationListener<T extends Notification> extends ProxyWidget {
const NotificationListener({
super.key,
required super.child,
this.onNotification,
});
/// 当适当类型的通知到达树中的此位置时调用。
/// Called when a notification of the appropriate type arrives at this
/// location in the tree.
///
/// 返回 true 取消通知冒泡。返回 false 以允许通知继续分派给更远的祖先
/// Return true to cancel the notification bubbling. Return false to
/// allow the notification to continue to be dispatched to further ancestors.
///
/// 通知的发送时间有所不同。主要有两种可能性:帧之间的分发和布局期间的分发
/// Notifications vary in terms of when they are dispatched. There are two
/// main possibilities: dispatch between frames, and dispatch during layout.
///
/// 对于在布局期间分派的通知(例如从LayoutChangedNotification继承的通知),调用State.setState来响应通知为时已晚(根据定义,布局当前发生在后代中,因为通知会在树中冒泡)。
/// 对于依赖于布局的小部件,请考虑使用LayoutBuilder
/// For notifications that dispatch during layout, such as those that inherit
/// from [LayoutChangedNotification], it is too late to call [State.setState]
/// in response to the notification (as layout is currently happening in a
/// descendant, by definition, since notifications bubble up the tree). For
/// widgets that depend on layout, consider a [LayoutBuilder] instead.
final NotificationListenerCallback<T>? onNotification;
@override
Element createElement() {
return _NotificationElement<T>(this);
}
}
|
可以发现,上面NotificationListener的注释是描述得不够详细的,下面结合它的注释&笔者的观点总结一下它的特性。
1、与InheritedWidget一样,NotificationListener同样继承自ProxyWidget,ProxyWidget用来提供了一个子Widget,而不是构建一个新的Widget。
2、NotificationListener可以指定一个泛型T,该泛型T必须是Notification的子类。注意:当显式指定泛型T时,如ScrollUpdateNotification,NotificationListener便只会接收ScrollUpdateNotification类型的通知。
3、NotificationListener需要传入一个onNotification参数,它的类型是NotificationListenerCallback,如果返回true时就会取消通知冒泡,否则允许通知继续分派给更远的祖先。
4、通知是Flutter中一个重要的机制,在Widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父级Widget都可以通过NotificationListener来监听通知。因此,Flutter中将这种由子级Widget向父级Widget的传递通知的机制称为通知冒泡(Notification Bubbling)。
5、通知冒泡和用户触摸事件(Listener)是相似的,但有一点不同:通知冒泡可以中止,但用户触摸事件不行。
6、Flutter中很多地方使用了通知,如下面将要介绍的PageView示例,它所创建的 _PageViewState 的build方法中就使用了NotificationListener监听滚动通知来实现onPageChanged功能,看下build方法源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(
widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
: widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
);
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
// 监听滚动通知
if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
// 获取当前页码
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
// 实现onPageChanged回调
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: Scrollable(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: widget.controller,
physics: physics,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: widget.controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
),
],
);
},
),
);
}
|
这里给出了NotificationListener的部分特性,主要是想让大家对NotificationListener有一个初级认知。
三、通知监听示例
在下面示例中,将通过NotificationListener来监听PageView的滚动通知的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyPage(title: 'PageView滚动通知监听'),
);
}
}
class MyPage extends StatefulWidget {
const MyPage({super.key, required this.title});
final String title;
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
final List<Widget> _widgets = [
const ColoredBox(color: Colors.redAccent),
const ColoredBox(color: Colors.greenAccent),
const ColoredBox(color: Colors.blueAccent),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: NotificationListener(
onNotification: (notification) {
switch (notification.runtimeType) {
case ScrollStartNotification:
debugPrint('开始滚动');
break;
case ScrollUpdateNotification:
debugPrint('正在滚动');
break;
case ScrollEndNotification:
debugPrint('滚动停止');
break;
case OverscrollNotification:
debugPrint('滚动到边界');
break;
case UserScrollNotification:
debugPrint('用户滚动');
break;
}
return false;
},
child: PageView.builder(
itemCount: _widgets.length,
itemBuilder: (context, index) => _widgets[index],
),
),
);
}
}
|
程序运行起来后,效果如下:
当滑动PageView时,上面这5个通知均会输出相应的日志:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
...
I/flutter (21018): 开始滚动
I/flutter (21018): 正在滚动
I/flutter (21018): 正在滚动
...
I/flutter (21018): 正在滚动
I/flutter (21018): 正在滚动
I/flutter (21018): 滚动停止
I/flutter (21018): 用户滚动
I/flutter (21018): 开始滚动
I/flutter (21018): 用户滚动
I/flutter (21018): 正在滚动
I/flutter (21018): 滚动到边界
I/flutter (21018): 滚动到边界
...
I/flutter (21018): 滚动到边界
I/flutter (21018): 滚动停止
I/flutter (21018): 用户滚动
|
通过查看它们的源码可以发现它们均继承自ScrollNotification类,并且不同类型的通知会包含不同的信息,比如ScrollUpdateNotification就有一个scrollDelta属性,它记录了移动的位移等。
通知 |
说明 |
ScrollStartNotification |
Scrollable小部件已开始滚动的通知 |
ScrollUpdateNotification |
Scrollable小部件已更改其滚动位置的通知 |
ScrollEndNotification |
Scrollable小部件已停止滚动的通知 |
OverscrollNotification |
Scrollable小部件尚未更改其滚动位置,因为更改会导致其滚动位置超出其滚动范围的通知 |
UserScrollNotification |
用户已更改滚动ScrollDirection或已停止滚动的通知 |
除了上面提到的和滚动相关的通知,还有一些其它的通知,例如KeepAliveNotification等,这些其它的通知和特定的功能相关。
四、分析通知源码
4.1、通知节点树的形成
当你使用NotificationListener这个Widget时,它所关联的Element为 _NotificationElement,对于每个 _NotificationElement 来说,它会持有一个父通知节点 _NotificationNode(上一个 _NotificationElement 所创建的通知节点)。
那么,在Element树的背后就会形成一颗通知节点树,来看下源码,了解下这颗通知节点树是怎么形成的。
通知节点树形成的关键在于attachNotificationTree方法,那么它是在哪里执行的?
当新创建的Element第一次添加到树中时,就会执行mount方法,在mount方法中会执行attachNotificationTree方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@mustCallSuper
void mount(Element? parent, Object? newSlot) {
assert(_lifecycleState == _ElementLifecycle.initial);
assert(_parent == null);
assert(parent == null || parent._lifecycleState == _ElementLifecycle.active);
assert(slot == null);
_parent = parent;
_slot = newSlot;
_lifecycleState = _ElementLifecycle.active;
_depth = _parent != null ? _parent!.depth + 1 : 1;
if (parent != null) {
// Only assign ownership if the parent is non-null. If parent is null
// (the root node), the owner should have already been assigned.
// See RootRenderObjectElement.assignOwner().
_owner = parent.owner;
}
assert(owner != null);
final Key? key = widget.key;
if (key is GlobalKey) {
owner!._registerGlobalKey(key, this);
}
_updateInheritance();
// 创建通知节点
attachNotificationTree();
}
|
以及在Element的activate方法中触发,也就是当先前停用的Element重新合并到Element树中时,也会执行attachNotificationTree方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@mustCallSuper
void activate() {
assert(_lifecycleState == _ElementLifecycle.inactive);
assert(owner != null);
final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
_lifecycleState = _ElementLifecycle.active;
// We unregistered our dependencies in deactivate, but never cleared the list.
// Since we're going to be reused, let's clear our list now.
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance();
// 创建通知节点
attachNotificationTree();
if (_dirty) {
owner!.scheduleBuildFor(this);
}
if (hadDependencies) {
didChangeDependencies();
}
}
|
看下Element的attachNotificationTree方法,对于在mount第一个 _NotificationElement 之前的Element来说,它的通知节点为null。
1
2
3
4
5
6
7
|
Element? _parent;
_NotificationNode? _notificationTree;
@protected
void attachNotificationTree() {
_notificationTree = _parent?._notificationTree;
}
|
另外,attachNotificationTree方法会被子类NotifiableElementMixin重写,而且NotificationListener所关联的 _NotificationElement 会混入NotifiableElementMixin。
因此,对于 _NotificationElement 来说,它会创建一个新的通知节点 _NotificationNode,看下NotifiableElementMixin的attachNotificationTree方法源码就知道。
1
2
3
4
5
6
7
|
mixin NotifiableElementMixin on Element {
@override
void attachNotificationTree() {
_notificationTree = _NotificationNode(_parent?._notificationTree, this);
}
}
|
再看下 _NotificationNode 源码可以发现, _NotificationNode 会持有父组件的 _NotificationNode(上一个 _NotificationElement 所创建的通知节点),也就是对应于parent参数,以及持有当前 _NotificationElement 所混入NotifiableElementMixin实例,也就是对应于current参数。
1
2
3
4
5
6
|
class _NotificationNode {
_NotificationNode(this.parent, this.current);
NotifiableElementMixin? current;
_NotificationNode? parent;
}
|
总结:当Element执行mount方法挂载,如果遇到第一个_NotificationElement时,就会为其创建一个_NotificationNode通知节点,该通知节点会持有上一个_NotificationElement所创建的通知节点,注意此时之前Element的通知节点均为null,所以该通知节点持有的上一个_NotificationElement所创建的通知节点为null。之后的Element继续执行mount方法挂载,这些Element持有的通知节点依然是第一个_NotificationElement所创建的通知节点。如果遇到第二个_NotificationElement时,就会为其创建一个_NotificationNode通知节点,该通知节点会持有上一个_NotificationElement所创建的通知节点,也就是第一个_NotificationElement所创建的通知节点,如此往复形成一颗通知节点树。
4.2、触发通知分发的过程
以上面PageView为例,之前看过它的build方法实现,内部会构建一个Scrollable,Scrollable是一个StatefulWidget,它所创建的State为ScrollableState。
在ScrollableState中,有一个 _updatePosition 方法,来看下它的源码,发现它会执行 _effectiveScrollController.createScrollPosition 来创建一个ScrollPosition。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 仅在肯定会触发重建的地方调用此方法
// Only call this from places that will definitely trigger a rebuild.
void _updatePosition() {
_configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context);
if (widget.physics != null) {
_physics = widget.physics!.applyTo(_physics);
} else if (widget.scrollBehavior != null) {
_physics = widget.scrollBehavior!.getScrollPhysics(context).applyTo(_physics);
}
final ScrollPosition? oldPosition = _position;
if (oldPosition != null) {
_effectiveScrollController.detach(oldPosition);
// It's important that we not dispose the old position until after the
// viewport has had a chance to unregister its listeners from the old
// position. So, schedule a microtask to do it.
scheduleMicrotask(oldPosition.dispose);
}
// 创建一个ScrollPosition
_position = _effectiveScrollController.createScrollPosition(_physics!, this, oldPosition);
assert(_position != null);
// 将ScrollPosition添加到ScrollController中的_positions中保存
_effectiveScrollController.attach(position);
}
|
看下ScrollController的createScrollPosition方法,这个方法有子类PageController实现,发现它内部创建了一个 _PagePosition 实例。
1
2
3
4
5
6
7
8
9
10
11
|
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
return _PagePosition(
physics: physics,
context: context,
initialPage: initialPage,
keepPage: keepPage,
viewportFraction: viewportFraction,
oldPosition: oldPosition,
);
}
|
通过跟踪 _PagePosition 的源码,可以发现它继承自ScrollPositionWithSingleContext,而ScrollPositionWithSingleContext又继承自ScrollPosition。
在ScrollPosition中,定义了几个通知分发的方法,供子类 _PagePosition 调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
// NOTIFICATION DISPATCH
/// Called by [beginActivity] to report when an activity has started.
void didStartScroll() {
activity!.dispatchScrollStartNotification(copyWith(), context.notificationContext);
}
/// Called by [setPixels] to report a change to the [pixels] position.
void didUpdateScrollPositionBy(double delta) {
activity!.dispatchScrollUpdateNotification(copyWith(), context.notificationContext!, delta);
}
/// Called by [beginActivity] to report when an activity has ended.
///
/// This also saves the scroll offset using [saveScrollOffset].
void didEndScroll() {
activity!.dispatchScrollEndNotification(copyWith(), context.notificationContext!);
saveOffset();
if (keepScrollOffset) {
saveScrollOffset();
}
}
/// Called by [setPixels] to report overscroll when an attempt is made to
/// change the [pixels] position. Overscroll is the amount of change that was
/// not applied to the [pixels] value.
void didOverscrollBy(double value) {
assert(activity!.isScrolling);
activity!.dispatchOverscrollNotification(copyWith(), context.notificationContext!, value);
}
/// Dispatches a notification that the [userScrollDirection] has changed.
///
/// Subclasses should call this function when they change [userScrollDirection].
void didUpdateScrollDirection(ScrollDirection direction) {
UserScrollNotification(metrics: copyWith(), context: context.notificationContext!, direction: direction).dispatch(context.notificationContext);
}
/// Dispatches a notification that the [ScrollMetrics] have changed.
void didUpdateScrollMetrics() {
assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks);
assert(_haveScheduledUpdateNotification);
_haveScheduledUpdateNotification = false;
if (context.notificationContext != null) {
ScrollMetricsNotification(metrics: copyWith(), context: context.notificationContext!).dispatch(context.notificationContext);
}
}
|
当PageController调用jumpToPage方法时,就会执行 _PagePosition 的jumpTo方法。
1
2
3
4
5
6
7
8
9
|
void jumpToPage(int page) {
final _PagePosition position = this.position as _PagePosition;
if (position._cachedPage != null) {
position._cachedPage = page.toDouble();
return;
}
position.jumpTo(position.getPixelsFromPage(page.toDouble()));
}
|
因为ScrollPosition定义了jumpTo抽象方法,由子类来实现,所以看下 _PagePosition 的jumpTo方法,但是跟踪源码后发现 _PagePosition 并没有实现jumpTo方法,所以看下它的父类ScrollPositionWithSingleContext的jumpTo方法。
1
2
3
4
5
6
7
8
9
10
11
12
|
@override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
}
goBallistic(0.0);
}
|
可以发现,在ScrollPositionWithSingleContext的jumpTo方法中,调用它的父类ScrollPosition的一些方法,它们是didStartScroll、didUpdateScrollPositionBy以及didEndScroll,之前讲过这几个方法是用来分发通知的。
这里以didStartScroll方法为例,看下它的源码,执行了ScrollActivity的dispatchScrollStartNotification方法。
1
2
3
|
void didStartScroll() {
activity!.dispatchScrollStartNotification(copyWith(), context.notificationContext);
}
|
看下ScrollActivity的dispatchScrollStartNotification方法,可以看到,执行了ScrollStartNotification的dispatch方法。
1
2
3
|
void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext? context) {
ScrollStartNotification(metrics: metrics, context: context).dispatch(context);
}
|
4.2、通知分发
看下ScrollStartNotification的dispatch方法,因为ScrollStartNotification的终极父类是Notification,所以执行的是Notification的dispatch方法。
在Notification的dispatch方法中,执行BuildContext的dispatchNotification方法,并把当前Notification实例,也就是ScrollStartNotification作为参数传入了dispatchNotification方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
abstract class Notification {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const Notification();
/// Start bubbling this notification at the given build context.
///
/// The notification will be delivered to any [NotificationListener] widgets
/// with the appropriate type parameters that are ancestors of the given
/// [BuildContext]. If the [BuildContext] is null, the notification is not
/// dispatched.
void dispatch(BuildContext? target) {
target?.dispatchNotification(this);
}
}
|
因为BuildContext只是一个句柄,所以看下它的实现类Element的dispatchNotification方法。可以看到,执行了 _notificationTree 的dispatchNotification方法。
1
2
3
4
5
6
|
_NotificationNode? _notificationTree;
@override
void dispatchNotification(Notification notification) {
_notificationTree?.dispatchNotification(notification);
}
|
看下 _NotificationNode的dispatchNotification方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class _NotificationNode {
_NotificationNode(this.parent, this.current);
NotifiableElementMixin? current;
_NotificationNode? parent;
void dispatchNotification(Notification notification) {
// 分析1
if (current?.onNotification(notification) ?? true) {
return;
}
// 分析2
parent?.dispatchNotification(notification);
}
}
|
这里分2种情况:
1、如果current为空,说明通知分发已完成,这时直接return,不执行分析2。
2、如果current不为空,说明通知分发正在进行中,可以发现调用了NotifiableElementMixin的onNotification方法,而 _NotificationElement 混入了NotifiableElementMixin,所以实际上调用了 _NotificationElement 的onNotification方法。
1
2
3
4
5
6
7
8
9
10
11
12
|
class _NotificationElement<T extends Notification> extends ProxyElement with NotifiableElementMixin {
_NotificationElement(NotificationListener<T> super.widget);
@override
bool onNotification(Notification notification) {
final NotificationListener<T> listener = widget as NotificationListener<T>;
if (listener.onNotification != null && notification is T) {
return listener.onNotification!(notification);
}
return false;
}
}
|
此处widget指的是NotificationListener,如果NotificationListener传入了onNotification回调并且notification是泛型T以及泛型T的子类时,就会通过调用listener.onNotification回调出去。
关于 _NotificationElement 的onNotification方法返回值有3种情况:
1、如果NotificationListener没有传入了onNotification回调,那么 _NotificationElement的onNotification就会返回false,此时会执行 _NotificationNode的dispatchNotification方法中的分析2部分。
2、如果NotificationListener传入了onNotification回调,并且该方法返回false,同样会执行 _NotificationNode 的dispatchNotification方法中的分析2部分。
3、如果NotificationListener传入了onNotification回调,并且该方法返回true,则不会执行 _NotificationNode的dispatchNotification方法中的分析2部分。
执行了parent?.dispatchNotification,也就是继续向上分发通知。
五、自定义通知
从源码分析中可以知道,通知分发是调用Notification的dispatch方法,那我们可以自定义一个Notification来进行分发。
这里还是改造之前的计数器示例,实现与在InheritedWidget中传入Listener的方式的同等效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyPage(title: '计数器'),
);
}
}
class MyPage extends StatefulWidget {
const MyPage({super.key, required this.title});
final String title;
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: NotificationListener<MyNotification>(
onNotification: (MyNotification notification) {
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(notification.msg)));
return false;
},
child: MyParent(
counter: _counter,
child: const MyChild(),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
tooltip: 'increment',
child: const Icon(Icons.add),
),
);
}
}
class MyParent extends InheritedWidget {
final int counter;
const MyParent({
super.key,
required this.counter,
required super.child,
});
static MyParent? maybeOf(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<MyParent>();
static MyParent of(BuildContext context) {
final MyParent? result = maybeOf(context);
assert(result != null, 'No MyParent found in context');
return result!;
}
@override
bool updateShouldNotify(covariant MyParent oldWidget) => counter != oldWidget.counter;
}
class MyChild extends StatefulWidget {
const MyChild({super.key});
@override
State<MyChild> createState() => _MyChildState();
}
class _MyChildState extends State<MyChild> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint('didChangeDependencies');
}
@override
Widget build(BuildContext context) {
debugPrint('build');
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${MyParent.of(context).counter}',
style: const TextStyle(
fontSize: 100,
color: Colors.black,
),
),
ElevatedButton(
onPressed: () => MyNotification(msg: 'Hello World!').dispatch(context),
child: const Text('Post'),
),
],
),
);
}
}
class MyNotification extends Notification {
final String msg;
MyNotification({required this.msg});
}
|