Routemaster

您好!Routemaster 是一个易于使用的 Flutter 路由器,它封装了 Navigator 2.0……并且有一个傻名字

Build codecov
pub

功能

  • 将 URL 映射到页面,简单声明式
  • 易于使用的 API:只需 Routemaster.of(context).push('/page')
  • 对标签页的嵌套导航支持非常简单
  • 多个路由映射:例如一个用于已登录用户,另一个用于未登录用户
  • 观察者,可轻松监听路由变化
  • 由 250 多个单元、组件和集成测试覆盖

这是应用程序中标签页和推送路由所需的所有路由设置

final routes = RouteMap(
  routes: {
    '/': (_) => CupertinoTabPage(
          child: HomePage(),
          paths: ['/feed', '/settings'],
        ),

    '/feed': (_) => MaterialPage(child: FeedPage()),
    '/settings': (_) => MaterialPage(child: SettingsPage()),
    '/feed/profile/:id': (info) => MaterialPage(
      child: ProfilePage(id: info.pathParameters['id'])
    ),
  }
);

void main() {
  runApp(
      MaterialApp.router(
        routerDelegate: RoutemasterDelegate(routesBuilder: (context) => routes),
        routeInformationParser: RoutemasterParser(),
      ),
  );
}

然后进行导航

Routemaster.of(context).push('/feed/profile/1');

……您可以在这个简单的应用程序示例中看到它的实际效果。

还有一个更高级的示例

我非常乐意收到您的任何反馈!请为 API 反馈创建一个 issue。

文档

从下面的快速入门开始,同时也可以查看API 参考Wiki常见问题解答


快速入门 API 导览

概述

Routemaster 根据当前路径生成页面。这是其基于路径的路由的核心概念。路径结构很重要。

它使用路径来决定页面的推送位置。这意味着路径需要与您期望的页面层次结构匹配。

例如

'/tabs': (route) => TabPage(child: HomePage(), paths: ['one', 'two']),

// First tab default page
'/tabs/one': (route) => MaterialPage(child: TabOnePage()),

// Second tab default page
'/tabs/two': (route) => MaterialPage(child: TabTwoPage()),

// Second tab sub-page: will be displayed in the 2nd tab because it
// starts with '/tabs/two'
'/tabs/two/subpage': (route) => MaterialPage(child: TabTwoPage()),

// Not a tab page: will not be displayed in in a tab
// because the path doesn't start with '/tabs/one' or '/tabs/two'
'/tabs/notInATab': (route) => MaterialPage(child: NotTabPage()),

任何以 /tabs/one/tabs/two 开头的子路径都将被推送到正确的标签页中。

当导航到 /tabs/two/subpage 时,TabPage 会被问到“嘿,你知道如何处理这个路径吗?”它会回答“当然!它以 /tabs/two 开头,所以它会进入我的第二个标签页”。

但是,导航到 /tabs/notInATab 将 **不会** 显示在标签页中,而是会推送到标签栏的顶部。

TabPage 会说“是的,抱歉,我不知道如何处理这个,它不匹配我的任何标签页路径”,然后它的父级会被要求处理它。

路径层次结构很重要,例如更改对话框的显示位置

路由

基础应用路由设置

MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    routesBuilder: (context) => RouteMap(routes: {
      '/': (routeData) => MaterialPage(child: PageOne()),
      '/two': (routeData) => MaterialPage(child: PageTwo()),
    }),
  ),
  routeInformationParser: RoutemasterParser(),
)

从页面内部导航

Routemaster.of(context).push('relative-path');
Routemaster.of(context).push('/absolute-path');

Routemaster.of(context).replace('relative-path');
Routemaster.of(context).replace('/absolute-path');

路径参数

// Path '/products/123' will result in ProductPage(id: '123')
RouteMap(routes: {
  '/products/:id': (route) => MaterialPage(
        child: ProductPage(id: route.pathParameters['id']),
      ),
  '/products/myPage': (route) => MaterialPage(MyPage()),
})

请注意,没有路径参数的路由具有更高的优先级,因此在上面的例子中
/products/myPage 将显示 MyPage

查询参数

// Path '/search?query=hello' results in SearchPage(query: 'hello')
RouteMap(routes: {
  '/search': (route) => MaterialPage(
        child: SearchPage(query: route.queryParameters['query']),
      ),
})

在 Widget 中获取当前路径信息

RouteData.of(context).path; // Full path: '/product/123?query=param'
RouteData.of(context).pathParameters; // Map: {'id': '123'}
RouteData.of(context).queryParameters; // Map: {'query': 'param'}

标签页

设置

RouteMap(
  routes: {
    '/': (route) => TabPage(
          child: HomePage(),
          paths: ['/feed', '/settings'],
        ),
    '/feed': (route) => MaterialPage(child: FeedPage()),
    '/settings': (route) => MaterialPage(child: SettingsPage()),
  },
)

主页面

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final tabPage = TabPage.of(context);

    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: tabPage.controller,
          tabs: [
            Tab(text: 'Feed'),
            Tab(text: 'Settings'),
          ],
        ),
      ),
      body: TabBarView(
        controller: tabPage.controller,
        children: [
          for (final stack in tabPage.stacks) PageStackNavigator(stack: stack),
        ],
      ),
    );
  }
}

Cupertino 风格的标签页

设置

RouteMap(
  routes: {
    '/': (route) => CupertinoTabPage(
          child: HomePage(),
          paths: ['/feed', '/settings'],
        ),
    '/feed': (route) => MaterialPage(child: FeedPage()),
    '/settings': (route) => MaterialPage(child: SettingsPage()),
  },
)

主页面

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final tabState = CupertinoTabPage.of(context);

    return CupertinoTabScaffold(
      controller: tabState.controller,
      tabBuilder: tabState.tabBuilder,
      tabBar: CupertinoTabBar(
        items: [
          BottomNavigationBarItem(
            label: 'Feed',
            icon: Icon(CupertinoIcons.list_bullet),
          ),
          BottomNavigationBarItem(
            label: 'Settings',
            icon: Icon(CupertinoIcons.search),
          ),
        ],
      ),
    );
  }
}

受保护的路由

如果验证失败,则显示默认的未找到页面

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : NotFound()

如果验证失败,则重定向到另一页面(会更改 URL)

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : Redirect('/no-access'),

如果验证失败,则显示另一页面(不更改 URL)

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : MaterialPage(child: CustomNoAccessPage())

404 页面

未知 URL 时显示的默认页面

RouteMap(
    onUnknownRoute: (route, context) {
        return MaterialPage(child: NotFoundPage());
    },
    routes: {
        '/': (_) => MaterialPage(child: HomePage()),
    },
)

重定向

将一个路由重定向到另一个路由

RouteMap(routes: {
    '/one': (routeData) => MaterialPage(child: PageOne()),
    '/two': (routeData) => Redirect('/one'),
})

将所有路由重定向到登录页面,用于未登录状态的路由映射

RouteMap(
  onUnknownRoute: (_) => Redirect('/'),
  routes: {
    '/': (_) => MaterialPage(child: LoginPage()),
  },
)

将路径参数从原始路径传递到重定向路径

RouteMap(routes: {
    '/user/:id': (routeData) => MaterialPage(child: UserPage(id: id)),
    '/profile/:uid': (routeData) => Redirect('/user/:uid'),
})

交换路由映射

您可以在运行时交换整个路由映射。

这对于根据用户是否登录而显示不同页面特别有用

final loggedOutMap = RouteMap(
  onUnknownRoute: (route, context) => Redirect('/'),
  routes: {
    '/': (_) => MaterialPage(child: LoginPage()),
  },
);

final loggedInMap = RouteMap(
  routes: {
    // Regular app routes
  },
);

MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    routesBuilder: (context) {
			// This will rebuild when AppState changes
      final appState = Provider.of<AppState>(context);
      return appState.isLoggedIn ? loggedInMap : loggedOutMap;
    },
  ),
  routeInformationParser: RoutemasterParser(),
);

导航观察者

class MyObserver extends RoutemasterObserver {
	// RoutemasterObserver extends NavigatorObserver and
	// receives all nested Navigator events
  @override
  void didPop(Route route, Route? previousRoute) {
    print('Popped a route');
  }

	// Routemaster-specific observer method
  @override
  void didChangeRoute(RouteData routeData, Page page) {
    print('New route: ${routeData.path}');
  }
}

MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    observers: [MyObserver()],
    routesBuilder: (_) => routeMap,
  ),
  routeInformationParser: RoutemasterParser(),
);

在没有 context 的情况下导航

app.dart

final routemaster = RoutemasterDelegate(
  routesBuilder: (context) => routeMap,
);

MaterialApp.router(
  routerDelegate: routemaster,
  routeInformationParser: RoutemasterParser(),
)

my_widget.dart

import 'app.dart';

void onTap() {
  routemaster.push('/blah');
}

Hero 动画

Hero 动画将在顶级导航器上自动工作(假设您使用的是 MaterialAppCupertinoApp)。

对于任何子导航器,您需要将 PageStackNavigator 包装在 HeroControllerScope 中,如下所示

HeroControllerScope(
  controller: MaterialApp.createMaterialHeroController(),
  child: PageStackNavigator(
    stack: pageStack,
  )
)

设计目标

  • 集成:与 Flutter Navigator 2.0 API 集成,不试图取代它。尝试具有非常 Flutter 风格的 API。
  • 可用:围绕用户场景/故事进行设计,例如Flutter storyboard 中的场景——此处有示例
  • 有主见:不提供 10 种方法来达成一个目标,而是为所有场景提供灵活性。
  • 专注:只关注导航,仅此而已。例如,不包含依赖项注入。

这个项目建立在 page_router 之上。

名称

最初的 Routemaster命名

A photo of a Routemaster bus

(照片由 Chris Sampson 拍摄,根据 CC BY 2.0 许可)

GitHub

https://github.com/tomgilder/routemaster