auto_route_library

Flutter 路由生成器。

简介

什么是 AutoRoute?

它是一个 Flutter 导航包,支持强类型参数传递,方便的深度链接,并使用代码生成来简化路由设置,这意味着它只需要极少的代码即可生成您应用中导航所需的一切。

为什么选择 AutoRoute?

如果您的应用需要深度链接或受保护的路由,或者只是需要一个干净的路由设置,您将需要使用命名/生成的路由,并且您最终会为中介参数类、检查必需参数、提取参数以及其他许多事情编写大量样板代码。AutoRoute 会为您完成这一切,甚至更多。

安装

dependencies:    
 auto_route: [latest-version]    
   
dev_dependencies:    
 auto_route_generator: [latest-version]    
 build_runner:    

设置和使用


创建一个占位符类并用 @MaterialAutoRouter 注解它,该注解需要一个路由列表作为必需参数。
注意:路由器的名称必须以 $ 开头,这样我们才能生成一个名称相同但去掉 $ 的类。

    
// @CupertinoAutoRouter    
// @AdaptiveAutoRouter    
// @CustomAutoRouter    
@MaterialAutoRouter(    
  replaceInRouteName: 'Page,Route',    
  routes: <AutoRoute>[    
    AutoRoute(page: BookListPage, initial: true),    
    AutoRoute(page: BookDetailsPage),    
  ],    
)    
class $AppRouter {}    

提示:您可以使用 replaceInRouteName 参数将生成的路由名称缩短,例如将 BookListPageRoute 缩短为 BookListRoute

现在只需运行生成器

使用 [watch] 标志来监视文件系统的变化并根据需要重新构建。

flutter packages pub run build_runner watch    

如果您希望生成器只运行一次然后退出,请使用

flutter packages pub run build_runner build    

完成设置

运行生成器后,您的路由类将会被生成,将其与 MaterialApp 连接起来。

class App extends StatlessWidget{
  // make sure you don't initiate your router
  // inside of the build function.
   final _appRouter = AppRouter();    
   
  Widget build(BuildContext context){    
      return MaterialApp.router(    
             routerDelegate: AutoRouterDelegeate(_appRouter),
             // or
             // routerDelegate: _appRouter.delegate(),    
             routeInformationParser: _appRouter.defaultRouteParser(),    
         ),    
  }    

生成的路由

对于每个声明的 AutoRoute,都会生成一个 PageRouteInfo 对象。这些对象包含路径信息以及从页面默认构造函数中提取的强类型页面参数。您可以将它们视为增强版的字符串路径片段。

class BookListRoute extends PageRouteInfo {    
  const BookListRoute() : super(name, path: '/books');    
    
  static const String name = 'BookListRoute';    
}    

如果声明的路由有子路由,AutoRoute 会在其构造函数中添加一个 children 参数,用于嵌套导航。更多信息请参见此处。

class UserRoute extends PageRouteInfo {    
   UserRoute({List<PagerouteInfo> children}) :    
    super(    
         name,     
         path: '/user/:id',    
         initialChildren: children);    
  static const String name = 'UserRoute';    
}    

AutoRouter 提供熟悉的 push、pop 等方法,使用生成的 PageRouteInfo 对象和路径来操作页面堆栈。

// get the scoped router by calling    
AutoRouter.of(context)    
// or using the extension    
context.router    
    
// adds a new entry to the pages stack    
router.push(const BooksListRoute())  
// or by using using paths  
router.pushNamed('/books')   

// removes last entry in stack and pushs provided route 
// if last entry == provided route page will just be updated
router.replace(const BooksListRoute())    
// or by using using paths  
router.replaceNamed('/books')  

// pops until provided route, if it already exists in stack    
// else adds it to the stack (good for web Apps).    
router.navigate(const BooksListRoute())  
// or by using using paths  
router.navigateNamed('/books')  

// adds a list of routes to the pages stack at once
router.pushAll([
   BooksListRoute(),
   BookDetailsRoute(id:1),
]);

// This's like providing a completely new stack as it rebuilds the stack
// with the list of passed routes
// entires might just update if alright exist
router.replaceAll([
   LoginRoute()
]);
// pops the last page unless stack has 1 entry    
context.router.pop();   
// keeps poping routes until predicate is satisfied
context.router.popUntil((route) => route.name == 'HomeRoute');
// a simplifed version of the above line
context.router.popUntilRouteWithName('HomeRoute');
// pops all routes down to the root
context.router.popUntilRoot();
     
// removes the top most page in stack even if it's the last
// remove != pop, it doesn't respect WillPopScopes it just 
// removes the entry.
context.router.removeLast(); 

// removes any route in stack that satisfis the predicate
// this works exactly like removing items from a regualar List
// <PageRouteInfo>[...].removeWhere((r)=>)
context.router.removeWhere((route) => );
    
// you can also use the common helper methods from context extension to navigate
context.pushRoute(const BooksListRoute());
context.replaceRoute(const BooksListRoute());
context.navigateTo(const BooksListRoute());
context.navigateNamedTo('/books');
context.popRoute();

传递参数

这是有趣的部分!AutoRoute 会自动为您检测和处理页面参数。生成的路由对象将传递您的页面所需的所有参数,包括路径/查询参数。

例如,以下页面小部件将接受一个 Book 类型的参数。

class BookDetailsPage extends StatelessWidget {    
 const BookDetailsRoute({required this.book});    
    
  final Book book; 
  ...    

注意: 默认值会被保留。必需字段也会被保留并妥善处理。

生成的 BookDetailsRoute 将会将其对应的页面传递相同的参数。

router.push(BookDetailsRoute(book: book));    

注意: 所有参数都会以命名参数的形式生成,无论其原始类型如何。

返回结果

您可以通过 pop completer 或将回调函数作为参数传递(就像传递对象一样)来返回结果。

1 - 使用 pop completer

var result = await router.push(LoginRoute());    

然后在您的 LoginPage 中使用结果 pop

router.pop(true);   

您会注意到我们没有指定结果类型,我们在这里处理的是动态值,这可能很危险,我个人不推荐这样做。

为了避免处理动态值,我们指定了我们期望页面返回的结果类型,即一个 bool 值。

AutoRoute<bool>(page: LoginPage), 

我们 push 并指定我们期望的结果类型

var result = await router.push<bool>(LoginRoute());    

当然,我们也以相同的类型 pop

router.pop<bool>(true);   

2 - 将回调函数作为参数传递。
我们只需要像下面这样向页面构造函数添加一个回调函数参数。

class BookDetailsPage extends StatelessWidget {    
 const BookDetailsRoute({this.book, required this.onRateBook});    
    
  final Book book;    
  final void Function(int) onRateBook;    
  ...    

生成的 BookDetailsRoute 将会将其对应的页面传递相同的参数。

context.router.push(    
      BookDetailsRoute(    
          book: book,    
          onRateBook: (rating) {    
           // handle result    
          }),    
    );    

如果您完成了结果的处理,请确保在 pop 页面时调用回调函数。

onRateBook(RESULT);    
context.router.pop();    

注意: 默认值会被保留。必需字段也会被保留并妥善处理。

使用路径

AutoRoute 中使用路径是可选的,因为 PageRouteInfo 对象是通过名称匹配的,除非使用 root delegate 中的 initialDeepLink 属性或 pushNamedreplaceNamednavigateNamed 方法以字符串形式 push。

如果您不指定路径,它将从页面名称生成,例如 BookListPage 的路径将是 'book-list-page'。如果 initial arg 设置为 true,路径将是 /,除非它是相对路径,那么它将是一个空字符串 ''

当开发 Web 应用程序或需要深度链接的原生应用程序时,您可能需要定义具有清晰易记名称的路径,这可以通过 AutoRoute 中的 path 参数来完成。

 AutoRoute(path: '/books', page: BookListPage),    

路径参数(动态段)

您可以通过在段前添加冒号来定义动态段。

 AutoRoute(path: '/books/:id', page: BookDetailsPage),    

从路径中提取路径参数并访问它们的最简单方法是将构造函数参数用 @PathParam('optional-alias') 注解,使用与段相同的别名/名称。

class BookDetailsPage extends StatelessWidget {    
  const BookDetailsPage({@PathParam('id') this.bookId});
  
  final int bookId;    
  ...    

现在,在浏览器中输入 /books/1 将会导航到 BookDetailsPage,并自动从路径中提取 bookId 参数并注入到您的 widget 中。

查询参数

查询参数的访问方式相同,只需用 @QueryParam('optional-alias') 注解用于存储查询参数值的构造函数参数,然后让 AutoRoute 完成其余工作。

您也可以使用作用域的 RouteData 对象来访问路径/查询参数。

 RouteData.of(context).pathParams;    
 // or using the extension    
 context.route.queryParams    

提示:如果您的参数名与路径/查询参数名相同,您可以使用 const @pathParam 或 @queryParam 而不传递 slug/别名。

class BookDetailsPage extends StatelessWidget {    
  const BookDetailsPage({@pathParam this.id});
  
  final int id;    
  ...    

重定向路径

路径可以使用 RedirectRoute 进行重定向。以下设置将在匹配到 / 时导航到 /books

<AutoRoute> [    
     RedirectRoute(path: '/', redirectTo: '/books'),    
     AutoRoute(path: '/books', page: BookListPage),    
 ]    

重定向初始路由时,可以通过将 /books 路径设置为 initial 来简化上述设置,auto_route 将自动为您生成所需的重定向代码。

<AutoRoute> [      
     AutoRoute(path: '/books', page: BookListPage, initial: true),    
 ]    

注意:RedirectRoutes 会被完全匹配。

通配符

auto_route 支持通配符匹配来处理无效或未定义的路径。

AutoRoute(path: '*', page: UnknownRoutePage)    
// it could be used with defined prefixes    
AutoRoute(path: '/profile/*', page: ProfilePage)    
// or it could be used with RedirectRoute    
RedirectRoute(path: '*', redirectTo: '/')    

注意: 请务必将您的通配符放在路由列表的末尾,因为路由是按顺序匹配的。

嵌套路由

使用 AutoRoute 嵌套路由就像填充父路由的 children 字段一样简单。在下面的示例中,UserProfilePageUserPostsPage 都是 UserPage 的嵌套子路由。

@MaterialAutoRouter(    
  replaceInRouteName: 'Page,Route',    
  routes: <AutoRoute>[    
    AutoRoute(    
      path: '/user/:id',    
      page: UserPage,    
      children: [    
        AutoRoute(path: 'profile', page: UserProfilePage),    
        AutoRoute(path: 'posts', page: UserPostsPage),    
      ],    
    ),    
  ],    
)    
class $AppRouter {}    

父页面 UserPage 将在 MaterialApp.router 提供的根路由小部件中渲染,但其子路由不会,这就是为什么我们需要在 UserPage 中放置一个 AutoRouter 小部件,在该小部件中需要渲染嵌套路由。

class UserPage extends StatelessWidget {    
  const UserPage({Key key, @pathParam this.id}) : super(key: key);    
  final int id;    
  @override    
  Widget build(BuildContext context) {    
    return Scaffold(    
      appBar: AppBar(title: Text('User $id')),     
      body: AutoRouter() // nested routes will be rendered here    
    );    
  }    
}    

现在如果我们导航到 /user/1,我们将看到一个 appBar 标题为 User 1 且主体为空的页面,为什么?因为我们还没有将任何路由推送到我们的嵌套 AutoRouter 中。但如果我们导航到 user/1/profileUserProfilePage 将会被推送到嵌套路由中,这就是我们将看到的内容。

如果我们想在 /users/1 显示其中一个子页面怎么办?我们可以通过给子页面一个空的路径 '' 来轻松实现。

   AutoRoute(    
      path: '/user/:id',    
      page: UserPage,    
      children: [    
        AutoRoute(path: '', page: UserProfilePage),    
        AutoRoute(path: 'posts', page: UserPostsPage),    
      ],    
    ),    

或者使用 RedirectRoute

   AutoRoute(    
      path: '/user/:id',    
      page: UserPage,    
      children: [    
        RedirectRoute(path: '', redirectTo: 'profile'),    
        AutoRoute(path: 'profile', page: UserProfilePage),    
        AutoRoute(path: 'posts', page: UserPostsPage),    
      ],    
    ),    

在这两种情况下,当我们导航到 /user/1 时,我们都会看到 UserProfilePage

查找正确的路由器

每个嵌套的 AutoRouter 都有自己的路由控制器来管理其内部堆栈,获取作用域控制器的最简单方法是使用 context。

在上面的示例中,UserPage 是一个根级别的堆栈条目,因此在其中任何地方调用 AutoRouter.of(context) 都会获取根路由控制器。

用于渲染嵌套路由的 AutoRouter 小部件会在小部件树中插入一个新的路由作用域,因此当嵌套路由请求作用域控制器时,它们将获得小部件树中最接近的父控制器,而不是根控制器。

class UserPage extends StatelessWidget {    
  const UserPage({Key key, @pathParam this.id}) : super(key: key);    
  final int id;    
  @override    
  Widget build(BuildContext context) {    
  // this will get us the root routing controller    
    AutoRouter.of(context);    
    return Scaffold(    
      appBar: AppBar(title: Text('User $id')),     
      // this inserts a new router scope into the widgets tree    
      body: AutoRouter()     
    );    
  }    
}    

这是一个简单的图示,有助于您可视化

从上图可以看出,可以通过调用 router.parent<T>() 来访问父路由控制器。我们使用泛型函数是因为我们有两种不同的路由控制器 StackRouterTabsRouter,其中一个可能是当前路由的父控制器,因此我们需要指定类型。

router.parent<StackRouter>() // this returns a the parent router as a Stack Routing controller    
router.parent<TabsRouter>() // this returns a the parent router as a Tabs Routing controller    

另一方面,获取根控制器不需要类型转换,因为它始终是一个 StackRouter

router.root // this returns the root router as a Stack Routing controller    

只要您有父路由器的访问权限,您也可以从其作用域外部获取内部路由器。

// assuming this's the root router    
AutoRouter.of(context).innerRouterOf<StackRouter>(UserRoute.name)    
// or use the short version     
AutoRouter.innerRouterOf(context, UserRoute.name);    

从上面的示例中访问 UserPage 的内部路由器。

class UserPage extends StatelessWidget {    
  final int id;    
    
  const UserPage({Key key, @pathParam this.id}) : super(key: key);    
    
  @override    
  Widget build(BuildContext context) {    
    return Scaffold(    
      appBar: AppBar(    
        title: Text('User $id'),    
        actions: [    
          IconButton(    
            icon: Icon(Icons.account_box),    
            onPressed: () {    
              // accessing the inner router from    
              // outside the scope    
              AutoRouter.innerRouterOf(context, UserRoute.name).push(UserPostsRoute());    
            },    
          ),    
        ],    
      ),    
      body: AutoRouter(), // we're trying to get access to this    
    );    
  }    
}    

注意:嵌套路由控制器会与父路由一起创建,因此只要它位于父路由(宿主页面)下方,就可以在没有 context 的情况下安全地访问它们。

路由守卫

您可以将路由守卫视为中间件或拦截器。路由在添加到堆栈之前必须经过其指定的守卫,守卫对于限制对某些路由的访问非常有用。

我们通过继承 auto_route 包中的 AutoRouteGuard 来创建一个路由守卫。
并在 onNavigation 方法中实现我们的逻辑。

class AuthGuard extends AutoRouteGuard {
 @override
 void onNavigation(NavigationResolver resolver, StackRouter router) {
 // the navigation is paused until resolver.next() is called with either 
 // true to resume/continue navigation or false to abort navigation
     if(authenitcated){
       // if user is autenticated we continue
        resolver.next(true);
      }else{
         // we redirect the user to our login page
         router.push(LoginRoute(onResult: (success){
                // if success == true the navigation will be resumed
                // else it will be aborted
               resolver.next(success);
          });
         }    
     }
}

重要resolver.next() 应该只被调用一次。

NavigationResolver 对象包含受保护的路由,您可以通过调用 resolver.route 属性来访问它,以及一个待处理路由列表(如果存在),可以通过调用 resolver.pendingRoutes 来访问。

现在我们将守卫分配给我们想要保护的路由。

 AutoRoute(page: ProfileScreen, guards: [AuthGuard]);

运行代码生成后,我们的路由将有一个必需的命名参数,称为 authGuard 或您的守卫名称。

// we pass our AuthGaurd to the generated router.
final _appRouter = AppRouter(authGuard: AuthGuard());

自定义

MaterialAutoRouter | CupertinoAutoRouter | AdaptiveAutoRouter
属性 默认值 定义
preferRelativeImports [bool] 如果为 true,则在可能的情况下使用相对导入。
replaceInRouteName [String] '' 用于替换生成路由名称中的常规单词(whatToReplacePattern,replacment)。

CustomAutoRouter

属性 默认值 定义
customRouteBuilder 用于提供自定义路由,它接受 BuildContext 和 CustomPage,并返回一个 PageRoute。
transitionsBuilder PageRouteBuilder 中 transitionsBuilder 属性的扩展。
opaque PageRouteBuilder 中 opaque 属性的扩展。
barrierDismissible PageRouteBuilder 中 barrierDismissible 属性的扩展。
durationInMilliseconds PageRouteBuilder 中 transitionDuration(millieSeconds) 属性的扩展。
reverseDurationInMilliseconds PageRouteBuilder 中 reverseDurationInMilliseconds(millieSeconds) 属性的扩展。

MaterialRoute | CupertinoRoute | AdaptiveRoute | CustomRoute

属性 默认值 定义
initial 将路径设置为 '/' 或 '',除非提供了路径,否则它将自动重定向到该路径。
path 如果未提供路径,则将使用自动生成的路径。
名称 这将是生成路由的名称,如果未提供,则会使用生成的名称。
usePathAsKey 如果为 true,则路径用作页面 key 而不是名称。
fullscreenDialog PageRoute 中 fullscreenDialog 属性的扩展。
maintainState PageRoute 中 maintainState 属性的扩展。

CupertinoRoute 特有 => CupertinoPageRoute

属性 默认值 定义
title CupertinoPageRoute 中 title 属性的扩展。

CustomRoute 特有 => PageRouteBuilder

属性 默认值 定义
transitionsBuilder PageRouteBuilder 中 transitionsBuilder 属性的扩展。
customRouteBuilder 用于提供自定义路由,它接受 BuildContext 和 CustomPage,并返回一个 PageRoute。
opaque PageRouteBuilder 中 opaque 属性的扩展。
barrierDismissible PageRouteBuilder 中 barrierDismissible 属性的扩展。
durationInMilliseconds PageRouteBuilder 中 transitionDuration(millieSeconds) 属性的扩展。
reverseDurationInMilliseconds PageRouteBuilder 中 reverseDurationInMilliseconds(millieSeconds) 属性的扩展。

自定义路由转场

要使用自定义路由转场,请使用 CustomRoute 并传入您的偏好设置。
TransitionsBuilder 函数需要作为静态/const 引用传递,该引用具有与 PageRouteBuilder 类中的 TransitionsBuilder 函数相同的签名。

CustomRoute(
page: LoginScreen,
//TransitionsBuilders class contains a preset of common transitions builders. 
transitionsBuilder: TransitionBuilders.slideBottom,
durationInMilliseconds: 400)

提示 使用 @CustomAutoRouter() 定义全局自定义路由转场。

当然,您可以使用自己的 transitionsBuilder 函数,只要它的函数签名相同。
该函数必须恰好接受 BuildContextAnimation<Double>Animation<Double> 和一个子 Widget,并且必须返回一个 Widget,通常您会像下面这样将子项包装在 Flutter 的一个转场小部件中。

Widget zoomInTransition(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
 // you get an animation object and a widget
 // make your own transition
    return ScaleTransition(scale: animation, child: child);
  }

现在将您的函数的引用传递给 CustomRoute

CustomRoute(page: ZoomInScreen, transitionsBuilder: zoomInTransition)

自定义路由构建器

您可以通过将 CustomRouteBuilder 函数传递给 CustomRoute 来使用自己的自定义路由。在代码生成中没有简单的方法可以强类型化静态函数,因此请确保您的自定义构建器签名与以下内容匹配。

typedef CustomRouteBuilder = Route<T> Function<T>(  
  BuildContext context, Widget child, CustomPage page);

现在我们像实现 TransitionsBuilder 函数一样实现我们的构建器函数,
这里最重要的部分是将页面参数传递给我们的自定义路由。

Route<T> myCustomRouteBuilder<T>(BuildContext context, Widget child, CustomPage<T> page){  
  return PageRouteBuilder(  
  fullscreenDialog: page.fullscreenDialog,  
  // this is important  
  settings: page,  
  pageBuilder: (,__,___)=> child);  
}

最后,我们将我们函数的引用传递给我们的 CustomRoute。

CustomRoute(page: CustomPage, customRouteBuilder: myCustomRouteBuilder)

更多文档即将推出

支持 auto_route

您可以通过在 Pub 上点赞并给 GitHub 上的仓库点星来支持 auto_route,分享关于如何改进某项功能的想法,或者报告您遇到的任何问题,当然,购买几杯咖啡将有助于加快开发进程。

GitHub

https://github.com/Milad-Akarie/auto_route_library