Flutter - 升级319之后页面多次rebuild?

欢迎关注微信公众号:FSA全栈行动 👋

BiliBili: www.bilibili.com/video/BV1Qm…

一、背景

上周一尝试从 3.16.9 升级 3.19.3,主要有两个原因:

一是安卓端有一个疑似造成崩溃率上涨的 bugFlutter 3.16 上出现,相关issue: #138947, 该 bug3.13 不会出现,在 3.17.pre 上得到修复,而 3.16 之后的下个正式版本是 3.19

二是苹果的隐私清单审核政策。

在苹果发布的【关于 App Store 提交的隐私更新】新闻中指出

自 3 月 13 日起: 如果你上传新 App 或更新 App 到 App Store Connect,且该 App 使用了需要声明批准原因的 API,但你未在 App 的隐私清单中提供批准原因,我们会通过电子邮件告知你。这是对 App Store Connect 中现有通知的补充。

自 5 月 1 日起: 你需要就你的 App 代码使用的所列 API 提供批准原因,才能将新 App 或更新 App 上传到 App Store Connect。如果你没有合理的原因使用某个 API,请寻找替代的方案。如果你添加了常用第三方 SDK 列表中所列的新版第三方 SDK,那么这些 API、隐私清单和签名要求将应用于该 SDK。请务必使用包含其隐私清单的 SDK 版本,并注意在将该 SDK 添加为二进制依赖项时也需要提供签名。

在苹果的【即将发布的第三方SDK要求】一文中,列举出需要隐私清单和签名的 SDK,其中就包含了 Flutter。为了符合该审核要求,Flutter3.19 开始包含了 PrivacyInfo.xcprivacy 这个隐私清单文件。

文件位于: github.com/flutter/eng…

二、踩坑

升到到 3.19.3 后发现,从 页面A 跳转到 页面B 和返回 页面A 时,页面Abuild 方法都会被执行,降回 3.16.9 则不会,这就很奇怪。后来发现是因为 页面A 间接使用了 ModalRoute.of

以下是可复现问题的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
diff复制代码class PageA extends StatefulWidget {
@override
State<PageA> createState() => _PageAState();
}

class _PageAState extends State<PageA> {
@override
Widget build(BuildContext context) {

// ==== 这里 ====
+ final arguments = ModalRoute.of(context)?.settings.arguments;
+ print("PageA arguments:$arguments");

return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('PageA'),
),
body: const SizedBox.shrink(),
);
}
}

三、探索

在经过一番摸索后,发现 ModalRoute3.19 上面有一个小修改~

相关 issue 是: #112567

issue 主要是涉及在 Web 端上按 Tab 键切换焦点的问题,后续有个 PR: #130841 解决了该问题。

PR 因内部测试原因进行了回滚,后再重新登陆,现PR: #134554

而在该 PR 中就对 ModalRoute 加了如下代码:

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
diff复制代码// packages/flutter/lib/src/widgets/routes.dart

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
...
+ @override
+ void didChangeNext(Route<dynamic>? nextRoute) {
+ super.didChangeNext(nextRoute);
+ changedInternalState();
+ }
+
+ @override
+ void didPopNext(Route<dynamic> nextRoute) {
+ super.didPopNext(nextRoute);
+ changedInternalState();
+ }
+
@override
void changedInternalState() {
super.changedInternalState();
- setState(() { /* internal state already changed */ });
- _modalBarrier.markNeedsBuild();
+ // No need to mark dirty if this method is called during build phase.
+ if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
+ setState(() { /* internal state already changed */ });
+ _modalBarrier.markNeedsBuild();
+ }
_modalScope.maintainState = maintainState;
}
...
}

didChangeNextdidPopNext 这两个方法对应的就是页面的 pushpop,现在在该 PR 中重写并调用了 changedInternalState 方法,在 changedInternalState 方法中调用了 setState

下面将以高亮的方式标出重点代码(不是新增代码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
diff复制代码abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
...
@protected
void setState(VoidCallback fn) {
if (_scopeKey.currentState != null) {
+ _scopeKey.currentState!._routeSetState(fn);
} else {
// The route isn't currently visible, so we don't have to call its setState
// method, but we do still need to call the fn callback, otherwise the state
// in the route won't be updated!
fn();
}
}
...
}

这个 ModalRoute 内的 setState 会使 _ModalScopeStatus_routeSetState 被调用,然后触发 _ModalScopeStatesetState,接着其 child: _ModalScopeStatus 就开始 rebuild 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
diff复制代码class _ModalScopeState<T> extends State<_ModalScope<T>> {
...
void _routeSetState(VoidCallback fn) {
if (widget.route.isCurrent && !_shouldIgnoreFocusRequest && _shouldRequestFocus) {
widget.route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode);
}
+ setState(fn);
}


@override
Widget build(BuildContext context) {
return AnimatedBuilder(
...
+ child: _ModalScopeStatus(
...
),
);
}
...
}

如下代码所示,_ModalScopeStatus 是一个 InheritedWidget,在经过一系列的处理后最终会走到其 InheritedElementupdate 方法,在 update 方法中通过调用 updateShouldNotify 来判断数据是否发生变化,进而决定是否通知相关依赖。

1
2
3
4
5
6
7
8
9
10
11
12
diff复制代码+ class _ModalScopeStatus extends InheritedWidget {
...

@override
+ bool updateShouldNotify(_ModalScopeStatus old) {
+ return isCurrent != old.isCurrent ||
canPop != old.canPop ||
impliesAppBarDismissal != old.impliesAppBarDismissal ||
route != old.route;
}
...
}
1
2
3
4
5
6
7
8
9
10
diff复制代码class InheritedElement extends ProxyElement {
...
@override
void updated(InheritedWidget oldWidget) {
+ if ((widget as InheritedWidget).updateShouldNotify(oldWidget)) {
super.updated(oldWidget);
}
}
...
}

_ModalScopeStatusisCurrent 表示当前页面是否处于最上层,所以在打开和关闭下一个页面时,其值必定切换,也就是 updateShouldNotify 必定返回 true,既而通知依赖(实际上就是找出一个个依赖进行标脏,然后等待 build 方法的重新调用)。

而我们在使用 ModalRoute.of 的时候,内部就是将当前页的 BuildContext 添加到依赖中,所以他这个改动就会影响到使用 ModalRoute.ofWidget,使其多次 rebuild

1
2
3
4
5
dart复制代码@optionalTypeArgs
static ModalRoute<T>? of<T extends Object?>(BuildContext context) {
final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
return widget?.route as ModalRoute<T>?;
}

四、解决方案

方案一:调整 ModalRoute.of

在当前版本中,of 的用意就是找到相应的 ModalRoute 并且创建依赖关系,当数据改变时会重新 build ,这是符合它期望用意的。

但是有些场景下我们并不希望有这个 “特性”,比如,我打开新页面后,通过 ModalRoute.of(context)?.settings.arguments 取路由参数,当前页面的取参,与跳转和关闭下个页面是没有任何关系的,所以这种场景下触发 rebuild 将毫无意义。

所以我提了个 PR: #145389, 给 ModalRoute.of 添加了 createDependency 参数,为开发者提供了是否创建依赖的选择。目前还在审核中~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dart复制代码  static ModalRoute<T>? of<T extends Object?>(
BuildContext context, {
bool createDependency = true,
}) {
_ModalScopeStatus? widget;
if (createDependency) {
widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
} else {
widget = context
.getElementForInheritedWidgetOfExactType<_ModalScopeStatus>()
?.widget as _ModalScopeStatus?;
}
return widget?.route as ModalRoute<T>?;
}

方案二:魔改源码

PR 是对 Tab 键切换焦点问题的修复,但对于移动端来说根本不算问题,因为用不上~ 😅 (当然,如果你们的用户有使用无障碍功能的,还需要自行斟酌一下)

如果这个问题到时还未解决(原 PR 的作者还在休假),那我们也可以先注释掉相关代码对 changedInternalState 的调用来应对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
diff复制代码// packages/flutter/lib/src/widgets/routes.dart

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
...
@override
void didChangeNext(Route<dynamic>? nextRoute) {
super.didChangeNext(nextRoute);
+ // changedInternalState();
}

@override
void didPopNext(Route<dynamic> nextRoute) {
super.didPopNext(nextRoute);
+ // changedInternalState();
}
}

提供一个补丁

1
2
3
4
5
6
7
8
9
shell复制代码# 进入你的 flutter 目录,比如我用的是 fvm 下载的 3.19.3
# 记得将 cd 后面的路径换成你自己电脑上的~
cd /Users/lxf/fvm/versions/3.19.3

# 下载补丁
curl -O https://raw.githubusercontent.com/LinXunFeng/flutter_assets/main/patch/01_rollbak_3_19_routes_change/0001-Roll-back-changes-to-routes.dart.patch

# 应用补丁
git apply 0001-Roll-back-changes-to-routes.dart.patch

五、最后

总而言之,距离5月1日(苹果强制要求添加隐私清单文件的期限)还有一个月,我们现在大可保持在 3.13 版本先用着,免得折腾,同时也祈祷快点修复该问题,然后顺利升级上去~

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%