beamer

处理您的应用程序路由,使其与浏览器 URL 同步等。Beamer 利用 Router 的强大功能,并为您实现所有底层逻辑。

快速入门

对于简单的应用程序,SimpleLocationBuilder 是一个合适的选择,它能以最少的代码实现一个可运行的应用程序。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: BeamerRouteInformationParser(),
      routerDelegate: BeamerRouterDelegate(
        locationBuilder: SimpleLocationBuilder(
          routes: {
            '/': (context) => HomeScreen(),
            '/books': (context) => BooksScreen(),
            '/books/:bookId': (context) => BookDetailsScreen()
          },
        ),
      ),
    );
  }
}

可以通过以下方式导航这些路由

Beamer.of(context).beamToNamed('/books/2');

// or
context.beamToNamed('/books/2');

并且可以通过以下方式访问路由属性(例如,用于构建 BookDetailsScreenbookId

Beamer.of(context).currentLocation.state.pathParameters['bookId'];

// or
context.currentBeamLocation.state.pathParameters['bookId'];

关键概念

对于一个相当大的应用程序,建议以“自然”形式使用 Beamer

BeamLocation

Beamer 中最重要的构造是 BeamLocation,它代表一个或多个页面的堆栈。
BeamLocation 具有 3 个重要作用

  • 知道它可以处理哪些 URI:pathBlueprints
  • 知道如何构建页面堆栈:pagesBuilder
  • 维护一个 state,该状态在前面两者之间提供链接

BeamLocation 是一个需要扩展的抽象类。拥有多个 BeamLocation 的目的是在架构上分离应用程序中不相关的“位置”。

例如,BooksLocation 可以处理所有与书籍相关的页面,而 ArticlesLocation 处理所有与文章相关的页面。根据这种作用域,BeamLocation 还有一个 builder,用于将整个页面堆栈包装在某些 Provider 中,以便相似的数据可以在相似页面之间共享。

这是一个 BeamLocation 的例子

class BooksLocation extends BeamLocation {
  BooksLocation(BeamState state) : super(state);

  @override
  List<String> get pathBlueprints => ['/books/:bookId'];

  @override
  List<BeamPage> pagesBuilder(BuildContext context, BeamState state) => [
        BeamPage(
          key: ValueKey('home'),
          child: HomeScreen(),
        ),
        if (state.uri.pathSegments.contains('books'))
          BeamPage(
            key: ValueKey('books'),
            child: BooksScreen(),
          ),
        if (state.pathParameters.containsKey('bookId'))
          BeamPage(
            key: ValueKey('book-${state.pathParameters['bookId']}'),
            child: BookDetailsScreen(
              bookId: state.pathParameters['bookId'],
            ),
          ),
      ];
}

BeamState

这是上面提到的 BeamLocationstate。它的作用是维护各种 URI 属性,例如 pathBlueprintSegments(选定 pathBlueprint 的片段,因为每个 BeamLocation 都支持其中许多)、pathParametersqueryParameters 和任意键值对 data。这些属性在构建页面时很重要,并且 BeamState 需要创建供浏览器使用的 uri

除了通过例如 beamToNamed('/books/3') 进行纯粹的命令式导航之外,它还提供了一种通过更改 BeamLocationstate 来进行声明式导航的方法。例如

Beamer.of(context).currentLocation.update(
  (state) => state.copyWith(
    pathBlueprintSegments: ['books', ':bookId'],
    pathParameters: {'bookId': '3'},
  ),
),

BeamState 可以扩展一个完全自定义的状态,该状态可用于 BeamLocation,例如

class BooksLocation extends BeamLocation<MyState> {...}

在这种情况下,CustomState 拥有一个 uri getter 很重要,这对于浏览器的 URL 栏是必需的。

Beaming

BeamLocation 之间或内部导航是通过“beaming”实现的。您可以将其视为“传送”(beaming)到您应用中的另一个位置。类似于 Navigator.of(context).pushReplacementNamed('/my-route'),但 Beamer 不限于单个页面,也不限于“push”本身。BeamLocation 包含一个任意的页面堆栈,当您 beam 到那里时,这些页面会被构建。使用 Beamer 感觉就像同时使用了 Navigator 的多个 push/pop 方法。

beaming 的例子

Beamer.of(context).beamTo(MyLocation());

// or with an extension on BuildContext
context.beamTo(MyLocation());
context.beamToNamed('/books/2');

// or more explicitly
context.beamTo(
  BooksLocation(
    BeamState(
      pathBlueprintSegments: ['books', ':bookId'],
      pathParameters: {'bookId': '2'},
    ),
  ),
),
context.beamToNamed(
  '/book/2',
  data: {'note': 'this is my favorite book'},
);

更新

一旦进入 BeamLocation,最好更新当前位置的状态。例如,从 /books/books/3(这两者都由 BooksLocation 处理)

context.currentBeamLocation.update(
  (state) => state.copyWith(
    pathBlueprintSegments: ['books', ':bookId'],
    pathParameters: {'bookId': '3'},
  ),
),

注意,当您尝试 beam 到当前所在的位置时,beaming 函数(beamToBeamToNamed)都会产生与 update 相同的影响,例如,如果您调用了上面的代码而不是 context.beamToNamed('/books/3')

Beaming Back

您访问过的所有 BeamLocation 都保存在 beamHistory 中。因此,可以“beam back”到上一个 BeamLocation。例如,在花费一些时间在 /books/books/3 上之后,假设您 beam 到 /articles,它由另一个 BeamLocation(例如 ArticlesLocation)处理。从那里,您可以回到离开时的状态,即 /books/3

context.beamBack();

注意,Beamer 会在您前进时从 beamHistory 中删除重复的位置。例如,如果您访问了 BooksLocationArticlesLocation,然后再次访问 BooksLocation,则 BooksLocation 的第一个实例将被从历史记录中删除,beamHistory 将是 [ArticlesLocation,BooksLocation] 而不是 [BooksLocation,ArticlesLocation,BooksLocation]。您可以通过将 BeamerRouterDelegate.removeDuplicateHistory 设置为 false 来关闭此功能。

注意,Beamer 可以集成 Android 的后退按钮,在当前 BeamLocation 的所有页面都弹出后,如果可能则执行 beamBack。这可以通过在 MaterialApp.router 中设置一个 back button dispatcher 来实现。

backButtonDispatcher: BeamerBackButtonDispatcher(delegate: routerDelegate)

您可以使用 context.canBeamBack 检查是否可以 beam back,甚至可以检查将要 beam back 的位置:context.beamBackLocation

用法

最后,我们回顾一些关于如何以及在哪里放置 Beamer 的注意事项。

应用于整个应用

要将 Beamer 应用于您的整个应用,您必须(根据官方文档)使用 .router 构造函数构建您的 *App widget,并为该构造函数(以及您所有常规的 *App 属性)提供

  • routeInformationParser,用于解析传入的 URI。
  • routerDelegate,用于控制 Navigator 的(重新)构建。

在这里,您使用 Beamer 对这些的实现 - BeamerRouteInformationParserBeamerRouterDelegate,并向它们传递您的 LocationBuilder

最简单的形式是,LocationBuilder 只是一个函数,它接收当前的 BeamState,并根据 URI 或其他状态属性返回一个自定义的 BeamLocation

class MyApp extends StatelessWidget {
  final routerDelegate = BeamerRouterDelegate(
    locationBuilder: (state) {
      if (state.uri.pathSegments.contains('books')) {
        return BooksLocation(state);
      }
      return HomeLocation(state);
    },
  );
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: routerDelegate,
      routeInformationParser: BeamerRouteInformationParser(),
      backButtonDispatcher:
          BeamerBackButtonDispatcher(delegate: routerDelegate),
    );
  }
}

如果您不想定义自定义的 LocationBuilder 函数,还有另外两个选项可用。

使用 BeamLocations 列表

您可以使用 BeamerLocationBuilderBeamLocation 列表。此构建器将根据每个 BeamLocationpathBlueprints 自动选择正确的 location。在这种情况下,请这样定义您的 BeamerRouterDelegate

final routerDelegate = BeamerRouterDelegate(
  locationBuilder: BeamerLocationBuilder(
    beamLocations: [
      HomeLocation(),
      BooksLocation(),
    ],
  ),
);

使用路由映射

正如在快速入门中所述,您可以使用 SimpleLocationBuilder 和路由映射以及 WidgetBuilder。这完全消除了对自定义 BeamLocation 的需求,但也为您提供了最少的自定义能力。不过,与所有其他选项一样,您的路径中的通配符和路径参数也受支持。

final routerDelegate = BeamerRouterDelegate(
  locationBuilder: SimpleLocationBuilder(
    routes: {
      '/': (context) => HomeScreen(),
      '/books': (context) => BooksScreen(),
      '/books/:bookId': (context) => BookDetailsScreen(
        bookId: context.currentBeamLocation.state.pathParameters['bookId'],
      ),
    },
  ),
);

在树的更深层

如果需要嵌套导航,Beamer 将被放置在树的更深层。在这种情况下,必须RootRouterDelegate 而不是 BeamerRouterDelegate 设置为最顶层的路由委托。然后,我们有两种选择

  • RootRouterDelegate 提供 homeBuilder,它将起到与 MaterialApp.home 相同的作用。当您需要一个带有某些导航栏的简单应用程序时,这很有用。
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: BeamerRouteInformationParser(),
      routerDelegate: RootRouterDelegate(
        homeBuilder: (context, uri) => Scaffold(
          body: Beamer(
            locationBuilder: _locationBuilder,
          ),
          ...
        ),
      ),
      ...
    );
  }
}
  • RootRouterDelegate 提供 locationBuilder,就像我们为 BeamerRouterDelegate 所做的那样,并将 Beamer 放置在这些 location 的某个深层位置(请参阅嵌套导航示例)。
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: BeamerRouteInformationParser(),
      routerDelegate: RootRouterDelegate(
        locationBuilder: BeamerLocationBuilder(
          beamLocations: [
            HomeLocation(),
            BooksLocation(),
            ArticlesLocation(),
          ],
        ),
      ),
      ...
    );
  }
}

一般说明

  • 在扩展 BeamLocation 时,需要实现两个方法:pathBlueprintspagesBuilder

    • pagesBuilder 返回一个页面堆栈,当您 beam 到那里时,Navigator 将使用它来构建;而 pathBlueprints 则用于 Beamer 确定哪个 BeamLocation 对应哪个 URI。
    • BeamLocation 在其 BeamState 中保留 URI 中的查询参数和路径参数。如果在 pathBlueprints 中使用 :,则表示您可能会从浏览器接收到路径参数。
  • BeamPage 的 child 是一个任意的 Widget,它代表您的应用程序屏幕/页面。

    • key 对于 Navigator 优化重建很重要。它应该是“页面状态”的唯一值。
    • BeamPage 默认创建 MaterialPageRoute,但可以通过将 BeamPage.type 设置为可用的 BeamPageType 之一来选择其他过渡。

注意,“Navigator 1.0”可以与 Beamer 一起使用。您可以轻松地使用 Navigator.of(context)pushpop 页面,但这些页面不会计入 URI。当需要显示某个不影响浏览器 URL 的信息/帮助页面时,这通常是必需的。当然,当在移动设备上使用 Beamer 时,这不成问题,因为没有 URL。

示例

书籍

这是这篇文章中关于 Flutter 新导航和路由系统的一个书籍示例的重现,您可以在其中了解许多关于 Navigator 2.0 的知识。有关此示例的完整应用程序代码,请参阅示例

example-books

高级书籍

为了进一步深入,我们添加了更多流程来演示 Beamer 的强大功能。完整代码可在此处获取

example-advanced-books

深度定位

您可以立即 beam 到应用程序中具有许多堆叠页面(深度链接)的位置,然后逐个弹出它们,或者直接 beamBack 到您来的地方。完整代码可在此处获取。请注意,beamTobeamBackOnPop 参数在此处可能很有用,用于覆盖 AppBarpop 操作为 beamBack

example-deep-location

ElevatedButton(
  onPressed: () => context.beamTo(DeepLocation('/a/b/c/d')),
  // onPressed: () => context.beamTo(DeepLocation('/a/b/c/d'), beamBackOnPop: true),
  child: Text('Beam deep'),
),

Location Builder

您可以覆盖 BeamLocation.builder 来为整个 location,即所有 pages 提供某些数据。完整代码可在此处获取

example-location-builder

// in your location implementation
@override
Widget builder(BuildContext context, Navigator navigator) {
  return MyProvider<MyObject>(
    create: (context) => MyObject(),
    child: navigator,
  );
}

Guard

您可以定义全局 guard(例如,身份验证 guard)或 location guard,以保护特定 location。完整代码可在此处获取

example-guards

  • 全局 Guard
BeamerRouterDelegate(
  guards: [
    BeamGuard(
      pathBlueprints: ['/books*'],
      check: (context, location) => AuthenticationStateProvider.of(context).isAuthenticated.value,
      beamTo: (context) => LoginLocation(),
    ),
  ],
  ...
),
  • Location (本地) Guard
// in your location implementation
@override
List<BeamGuard> get guards => [
  BeamGuard(
    pathBlueprints: ['/books/*'],
    check: (context, location) => location.pathParameters['bookId'] != '2',
    showPage: forbiddenPage,
  ),
];

Beamer Widget

当您需要嵌套导航时,将 Beamer 放置到 Widget 树中的示例。

example-bottom-navigation-mobile

example-bottom-navigation-multiple-beamers

example-nested-navigation

class MyApp extends StatelessWidget {
  final _beamerKey = GlobalKey<BeamerState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: BeamerRouteInformationParser(),
      routerDelegate: RootRouterDelegate(
        homeBuilder: (context, uri) => Scaffold(
          body: Beamer(
            key: _beamerKey,
            routerDelegate: BeamerRouterDelegate(
              locationBuilder: (state) {
                if (state.uri.pathSegments.contains('books')) {
                  return BooksLocation(state);
                }
                return ArticlesLocation(state);
              },
            ),
          ),
          bottomNavigationBar: BottomNavigationBarWidget(
            beamerKey: _beamerKey,
          ),
        ),
      ),
    );
  }
}
class MyAppState extends State<MyApp> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: IndexedStack(
          index: _currentIndex,
          children: [
            Beamer(
              routerDelegate: BeamerRouterDelegate(
                locationBuilder: (state) => ArticlesLocation(state),
              ),
            ),
            Container(
              color: Colors.blueAccent,
              padding: const EdgeInsets.all(32.0),
              child: Beamer(
                routerDelegate: BeamerRouterDelegate(
                  locationBuilder: (state) => BooksLocation(state),
                ),
              ),
            ),
          ],
        ),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: _currentIndex,
          items: [
            BottomNavigationBarItem(label: 'A', icon: Icon(Icons.article)),
            BottomNavigationBarItem(label: 'B', icon: Icon(Icons.book)),
          ],
          onTap: (index) => setState(() => _currentIndex = index),
        ),
      ),
    );
  }
}
...

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: BeamerRouteInformationParser(),
      routerDelegate: RootRouterDelegate(
        locationBuilder: (state) => HomeLocation(state),
      ),
    );
  }
}

...


class HomeLocation extends BeamLocation {
  @override
  List<String> get pathBlueprints => ['/*'];

  @override
  List<BeamPage> pagesBuilder(BuildContext context) => [
        BeamPage(
          key: ValueKey('home'),
          child: HomeScreen(),
        )
      ];
}

...

class HomeScreen extends StatelessWidget {
  final _beamerKey = GlobalKey<BeamerState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Row(
        children: [
          Container(
            color: Colors.blue[300],
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                ElevatedButton(
                  onPressed: () => _beamerKey.currentState.routerDelegate
                      .beamToNamed('/books'),
                  child: Text('Books'),
                ),
                SizedBox(height: 16.0),
                ElevatedButton(
                  onPressed: () => _beamerKey.currentState.routerDelegate
                      .beamToNamed('/articles'),
                  child: Text('Articles'),
                ),
              ],
            ),
          ),
          Container(width: 1, color: Colors.blue),
          Expanded(
            child: Beamer(
              key: _beamerKey,
              routerDelegate: BeamerRouterDelegate(
                locationBuilder: (state) {
                  if (state.uri.pathSegments.contains('books')) {
                    return BooksLocation(state);
                  }
                  if (state.uri.pathSegments.contains('articles')) {
                    return ArticlesLocation(state);
                  }
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

example-animated-rail

GitHub

https://github.com/slovnicki/beamer