新手
几个实用的 Flutter 工具,极其简单的 UriRouter 用于基于 Uri 的导航器,或 BuildTracker 用于跟踪 widget 重建以及导致它们重建的原因。
UriRouter
极其简单的基于 Uri 的页面路由器。
class BooksPage extends StatelessWidget {
static final route = UriRoute(
path: '/books/:id',
pageBuilder: (context, params) => BooksPage(id: params.pathParams['id']!),
);
static String path(String id) => route.build(pathParams: <String, String>{'id': id});
...
}
final router = UriRouter(routes: [
BooksPage.route,
]);
runApp(MaterialApp(onGenerateRoute: router.generateRoute));
Navigator.pushNamed(context, BooksPage.path('42'));
Hooks
一些实用的 hooks
useDisposable:管理需要被销毁的对象useRebuild:手动触发HookWidet/HookBuilder的重建useVariable:轻量级 hook,用于创建不会在更改时触发重建的变量(可变值)useListener/useValueListener:将回调附加到Listenable/ValueListenable,而不会在它们收到通知时触发重建useAsyncValue:将Future转换为AsyncValueuseLastValidAsyncData:记住最近有效的AsyncData
Providers
一些实用的 providers
lastPointerEventProvider:跟踪最后一个[PointerEvents 活动指针globalPositionProvider:跟踪BuildContextsRenderBox的全局位置
PointerIndicator
PointerIndicator(child: ...)
PointerIndicator 显示 PointerEvents 的位置,从而允许记录屏幕(包括“手指”)。
BuildTracker
void main() {
// initialize `TrackingBuildOwnerWidgetsFlutterBinding` to enable tracking
TrackingBuildOwnerWidgetsFlutterBinding.ensureInitialized();
// initialize `BuildTracker`
final tracker = BuildTracker(printBuildFrameIncludeRebuildDirtyWidget: false);
// print top 10 stacks leading to rebuilds every 10 seconds
Timer.periodic(const Duration(seconds: 10), (_) => tracker.printTopScheduleBuildForStacks());
// run
runApp(...);
}
许多人花费了数小时来弄清楚为什么 Flutter 不断地重建他们应用程序中的数十个 widget。BuildTracker 可以帮助跟踪每一帧重建了哪些 widget,更重要的是,还追踪了导致它们重建的原因。
Flutter 的渲染管线通常处于空闲状态,直到 widget 树中的某个 widget 被标记为脏(dirty),例如通过调用 StatefulWidget 的 State 上的 setState。在这种情况下,Flutter 将安排在下一帧中构建该 widget。然而,在实际构建下一帧之前,可能会有更多 widget 被标记为脏。我们将所有这些 widget 称为后续帧的“构建根”。
当 Flutter 最终构建下一帧时,它会从构建根开始,这些构建根可能会在递归向下滴入 widget 树的过程中触发更多 widget 的构建。
对于每一帧,BuildTracker 会打印所有重建的 widget 以及每个构建根的堆栈跟踪。
考虑以下示例测试用例
void main() {
TrackingBuildOwnerAutomatedTestWidgetsFlutterBinding.ensureInitialized();
final tracker = BuildTracker(enabled: false);
testWidgets('Test build frames', (tester) async {
tracker.enabled = true;
final text = ValueNotifier('');
debugPrint('# `tester.pumpWidget(...)`');
debugPrint('');
await tester.pumpWidget(
ValueListenableBuilder<String>(
valueListenable: text,
builder: (_, value, child) => Directionality(
textDirection: TextDirection.ltr,
child: Text(value),
),
),
);
debugPrint("# Looping `text.value`");
debugPrint('');
for (var i = 0; i < 10; i++) {
text.value = '$i';
await tester.pump();
}
debugPrint('# test end');
debugPrint('');
tracker.enabled = false;
tracker.printTopScheduleBuildForStacks();
});
}
如果我们运行该示例,BuildTracker 会生成包含重建和脏 widget 的 markdown 格式日志。完整的输出可以在 这里 找到。
例如,在 text.value = '0' 之后,我们有
已构建的 widgets
[root]ValueListenableBuilder<String> ← [root]Directionality ← ValueListenableBuilder<String> ← [root]Text ← Directionality ← ValueListenableBuilder<String> ← [root]
被标记为脏(构建根)的 widgets
[root]
Stack trace #1beada3
Element.markNeedsBuild package:flutter/src/widgets/framework.dart 4157:12
RenderObjectToWidgetAdapter.attachToRenderTree package:flutter/src/widgets/binding.dart 1102:15
WidgetsBinding.attachRootWidget package:flutter/src/widgets/binding.dart 934:7
WidgetTester.pumpWidget.<fn> package:flutter_test/src/widget_tester.dart 520:15
_CustomZone.run dart:async
TestAsyncUtils.guard package:flutter_test/src/test_async_utils.dart 72:41
WidgetTester.pumpWidget package:flutter_test/src/widget_tester.dart 519:27
* main.<fn> test/build_tracker_test.dart 17:18
因此,我们可以轻松地发现 test/build_tracker_test.dart 25:10 是触发该帧的实际位置,即 text.value = '0'。
此外,调用 BuildTracker.printTopScheduleBuildForStacks 会打印导致重建的顶级堆栈跟踪
前 10 个 scheduleBuildFor 堆栈跟踪(构建根)
10 次
Stack trace #16e7ece8
State.setState package:flutter/src/widgets/framework.dart 1287:15
_ValueListenableBuilderState._valueChanged package:flutter/src/widgets/value_listenable_builder.dart 182:5
ChangeNotifier.notifyListeners package:flutter/src/foundation/change_notifier.dart 243:25
ValueNotifier.value= package:flutter/src/foundation/change_notifier.dart 309:5
* main.<fn> test/build_tracker_test.dart 30:12
...
1 次
Stack trace #1beada3
Element.markNeedsBuild package:flutter/src/widgets/framework.dart 4157:12
RenderObjectToWidgetAdapter.attachToRenderTree package:flutter/src/widgets/binding.dart 1102:15
WidgetsBinding.attachRootWidget package:flutter/src/widgets/binding.dart 934:7
WidgetTester.pumpWidget.<fn> package:flutter_test/src/widget_tester.dart 520:15
_CustomZone.run dart:async
TestAsyncUtils.guard package:flutter_test/src/test_async_utils.dart 72:41
WidgetTester.pumpWidget package:flutter_test/src/widget_tester.dart 519:27
* main.<fn> test/build_tracker_test.dart 17:18
...
PeriodicListenable
final timer30 = useDisposable<PeriodicListenable>(() => PeriodicListenable(const Duration(seconds: 30)), (_) => _.dispose());
useListener(timer60, callInitially: false, callback: () => context.refresh(daaProvider));
PeriodicListenable 可用于创建一个 Listenable,该 Listenable 会周期性地通知其侦听器。