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');
并且可以通过以下方式访问路由属性(例如,用于构建 BookDetailsScreen 的 bookId)
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
这是上面提到的 BeamLocation 的 state。它的作用是维护各种 URI 属性,例如 pathBlueprintSegments(选定 pathBlueprint 的片段,因为每个 BeamLocation 都支持其中许多)、pathParameters、queryParameters 和任意键值对 data。这些属性在构建页面时很重要,并且 BeamState 需要创建供浏览器使用的 uri。
除了通过例如 beamToNamed('/books/3') 进行纯粹的命令式导航之外,它还提供了一种通过更改 BeamLocation 的 state 来进行声明式导航的方法。例如
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 函数(beamTo 和 BeamToNamed)都会产生与 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 中删除重复的位置。例如,如果您访问了 BooksLocation、ArticlesLocation,然后再次访问 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 对这些的实现 - BeamerRouteInformationParser 和 BeamerRouterDelegate,并向它们传递您的 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 列表
您可以使用 BeamerLocationBuilder 和 BeamLocation 列表。此构建器将根据每个 BeamLocation 的 pathBlueprints 自动选择正确的 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时,需要实现两个方法:pathBlueprints和pagesBuilder。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) 来 push 或 pop 页面,但这些页面不会计入 URI。当需要显示某个不影响浏览器 URL 的信息/帮助页面时,这通常是必需的。当然,当在移动设备上使用 Beamer 时,这不成问题,因为没有 URL。
示例
书籍
这是这篇文章中关于 Flutter 新导航和路由系统的一个书籍示例的重现,您可以在其中了解许多关于 Navigator 2.0 的知识。有关此示例的完整应用程序代码,请参阅示例。

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

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

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 提供某些数据。完整代码可在此处获取。

// 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。完整代码可在此处获取。

- 全局 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 树中的示例。



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,
),
),
),
);
}
}
- 带有多个 Beamers 的底部导航示例 (WIP)
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);
}
},
),
),
),
],
),
);
}
}
