新手

几个实用的 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 转换为 AsyncValue
  • useLastValidAsyncData:记住最近有效的 AsyncData

Providers

一些实用的 providers

  • lastPointerEventProvider:跟踪最后一个 [PointerEvents 活动指针
  • globalPositionProvider:跟踪 BuildContexts RenderBox 的全局位置

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),例如通过调用 StatefulWidgetState 上的 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 会周期性地通知其侦听器。

GitHub

https://github.com/derolf/flutter_noob