[译]Flutter Favorite之路由包go_router - 高级路由 - 异步数据 & 导航构建器 & Web历史

1,069 阅读3分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

本文翻译自

水平太烂~ 翻到怀疑人生~ 不吝赐教~

[译]Flutter Favorite之路由包go_router - 基础篇 - 掘金 (juejin.cn)

异步数据

有时候,想要异步加载数据。这种情况下,你需要给界面传递参数来展示数据,并让它自己进行查找:

late final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreenWithAsync(),
      routes: [
        GoRoute(
          path: 'family/:fid',
          builder: (context, state) => FamilyScreenWithAsync(
            fid: state.params['fid']!,
          ),
          routes: [
            GoRoute(
              path: 'person/:pid',
              builder: (context, state) => PersonScreenWithAsync(
                fid: state.params['fid']!,
                pid: state.params['pid']!,
              ),
            ),
          ],
        ),
      ],
    ),
  ],
);

界面会使用任意内容来查找。例如,下面展示了使用 Repository 模式 和 Flutter FutureBuilder 来加载和展示数据:

class FamilyScreen extends StatefulWidget {
  const FamilyScreen({required this.fid, Key? key}) : super(key: key);
  final String fid;

  @override
  State<FamilyScreen> createState() => _FamilyScreenState();
}

class _FamilyScreenState extends State<FamilyScreen> {
  Future<Family>? _future;

  @override
  void initState() {
    super.initState();
    _fetch();
  }

  @override
  void didUpdateWidget(covariant FamilyScreen oldWidget) {
    super.didUpdateWidget(oldWidget);

    // refresh cached data
    if (oldWidget.fid != widget.fid) _fetch();
  }

  void _fetch() => _future = App.repo.getFamily(widget.fid);

  @override
  Widget build(BuildContext context) => FutureBuilder<Family>(
        future: _future,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Scaffold(
              appBar: AppBar(title: const Text('Loading...')),
              body: const Center(child: CircularProgressIndicator()),
            );
          }

          if (snapshot.hasError) {
            return Scaffold(
              appBar: AppBar(title: const Text('Error')),
              body: SnapshotError(snapshot.error!),
            );
          }

          assert(snapshot.hasData);
          final family = snapshot.data!;
          return Scaffold(
            appBar: AppBar(title: Text(family.name)),
            body: ListView(
              children: [
                for (final p in family.people)
                  ListTile(
                    title: Text(p.name),
                    onTap: () => context.go(
                      '/family/${family.id}/person/${p.id}',
                    ),
                  ),
              ],
            ),
          );
        },
      );
}

该代码展示了一个获取数据时的进度指示器和获取失败时的错误。

async.gif

完整细节参考 异步示例

导航构建器

有时候,有必要在 Navigator 上、在 MaterialApp/CupertinoApp 下插入组件。例如:插入一个 Provider ,需要访问 App 的 context (上下文) 来获取当前的区域语言和本地化,在导航之外来构建UI或者使用自己的内容 (在本文范围之外)完全替换 Navigator

为了这些目的,需要使用 GoRouter 构造器的 navigatorBuilder 参数。这和 MaterialAppbuilder 参数类似,但是能够访问 MaterialApp 提供的基础设施。

一个放置一些数据Provider组件的示例:

final _router = GoRouter(
  routes: ...,

  // add a wrapper around the navigator to put loginInfo into the widget tree
  navigatorBuilder: (context, state, child) =>
    ChangeNotifierProvider<LoginInfo>.value(
      value: loginInfo,
      builder: (context, _) => child,
    ),
);

一个使用 navigatorBuilder 的有更趣的示例如下,它会在每个页面添加一个漂浮按钮允许快捷登出:

final _router = GoRouter(
  routes: ...,

  // add a wrapper around the navigator to:
  // - put loginInfo into the widget tree, and to
  // - add an overlay to show a logout option
    navigatorBuilder: (context, state, child) =>
        ChangeNotifierProvider<LoginInfo>.value(
      value: loginInfo,
      builder: (context, _) =>
        loginInfo.loggedIn ? AuthOverlay(child: child) : child;
      },
    ),
);

该示例会检查 navigatorBuilder 中的登录状态:

  • 如果用户已登录,AuthOverlay 组件实例会创建,它包装了 Navigator ,通过 child 参数传递给 navigatorBuilder ,并在每个页面提供一个登出按钮。

  • 如果用户未登录,则通过 child 参数返回 Navigator

AuthOverlay 在 Stack 中展示登出按钮和 Navigator

class AuthOverlay extends StatelessWidget {
  const AuthOverlay({required this.child, Key? key}) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) => Stack(
        children: [
          child,
          Positioned(
            top: 90,
            right: 4,
            child: ElevatedButton(
              onPressed: () {
                context.read<LoginInfo>().logout();
                context.goNamed('home'); // clear out the `from` query param
              },
              child: const Icon(Icons.logout),
            ),
          ),
        ],
      );
}

动作如下:

nav_builder.gif

Web历史

有时候,导航时不希望浏览器追踪历史记录。这种情况下, go_router 支持 Router.neglect

ElevatedButton(
  // turn off history tracking in the browser for this navigation
  onPressed: () => Router.neglect(context, () => context.go('/page2'))},
  ...
),

使用 Router.neglect 会用阻止Flutter路由添加此页面到浏览器历史中。如果想要浏览器停止应用中所有的历史记录追踪,需要设置 GoRouter 构造器的 routerNeglect 参数。这会废止所有使用 go_router 导航的历史。

final _router = GoRouter(
  // turn off history tracking in the browser for all navigation
  routerNeglect: true,
  ...
);

即使当你指示路由忽略添加导航到浏览器的历史时,深度链接和动态链接也会正常运作,浏览器的地址栏也会通过应用在你导航时更新。它只会影响到浏览器的返回按钮。