Flutter自动路由插件auto_route详解

一、简介

在Flutter应用开发过程中,多个页面的跳转需要使用路由,除了官方提供的Navigator外,我们还可以使用一些第三方路由框架来实现页面的管理和导航,如Fluro、Frouter等。不过,今天要给大家介绍的是另一款路由框架auto_route。

image.png

auto_route是一个设计精简、低耦合的路由框架,支持自动生成路由代码、动态添加路由、以及路由的参数传递等功能。相比其他的路由框架,auto_route的使用也更加简洁。

二、基本使用

2.1 安装插件

和其他Flutter插件的使用流程一样,使用之前需要先在项目中安装auto_route插件,安装的的脚本如下:

dependencies:              
  auto_route: [latest-version]              
              
dev_dependencies:              
  auto_route_generator: [latest-version]              
  build_runner:       
复制代码

2.2 定义路由表

接下来,定义一个路由表的管理类,用来同意管理应用的路由,需要使用@MaterialAutoRouter注解进行标识,如下。       

@MaterialAutoRouter(              
  replaceInRouteName: 'Page,Route',              
  routes: <AutoRoute>[              
    AutoRoute(page: BookListPage, initial: true),              
    AutoRoute(page: BookDetailsPage),              
  ],              
)              
class $AppRouter {}
复制代码

要生成路由文件的一部分而不是独立的 AppRouter 类,只需将 Part 指令添加到AppRouter 并扩展生成的私有路由器即可。

part 'app_router.gr.dart';      
        
@MaterialAutoRouter(              
  replaceInRouteName: 'Page,Route',              
  routes: <AutoRoute>[              
    AutoRoute(page: BookListPage, initial: true),              
    AutoRoute(page: BookDetailsPage),              
  ],              
)              
       
class AppRouter extends _$AppRouter{}
复制代码

接下来,我们使用build_runner提供的命令即可生成路由代码。

//自动刷新路由表
flutter packages pub run build_runner watch       
//生成路由代码
flutter packages pub run build_runner build 
复制代码

等待命令执行完成之后,即可在app_router.dart同级的目录下生成一个app_route.gr.dart文件,也是我们执行路由跳转时需要用到的代码。最后,我们打开main.dart入口文件,然后注册路由文件。

class App extends StatelessWidget {      
          
  final _appRouter = AppRouter();      
      
  @override      
  Widget build(BuildContext context){      
    return MaterialApp.router(      
      routerDelegate: _appRouter.delegate(),      
      routeInformationParser: _appRouter.defaultRouteParser(),      
    );      
  }      
}
复制代码

2.3 生成路由

当然,auto_route还支持为每个声明的 AutoRoute 生成一个 PageRouteInfo 对象,这些对象包含路径信息以及从页面的默认构造函数中提取的强类型页面参数。

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

并且,如果声明的路由有子路由,那么 AutoRoute 会在其构造函数中添加一个子参数,如下。

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

2.4 路由跳转

和其他的路由框架一样,AutoRouter 也提供常见的 push、pop 和 remove 方法。比如,我们要打一个新的页面,那么可以使用下面

AutoRouter.of(context).replaceAll([const LoginRoute()]);  //LoginRoute为路由


//或者
AutoRouter.of(context).navigate(const BooksListRoute())       
复制代码

如果我们使用的是命名路由,那么可以使用navigateNamed()方法,如下。

AutoRouter.of(context).pushNamed('/books') ;
复制代码

当然,很多时候,路由的跳转还会涉及很多的参数传递,那么对于需要传递参数的路由,我们需要怎么处理呢?对于参数传递,我们可以在目标路由页面使用构造函数的方式,然后再用AutoRouter进行传递。

AutoRouter.of(context).pushAll([IndexRoute(login: true)]);
复制代码

除了跳转,我们还可能需要处理路由弹栈的场景,对于弹栈,需要用到pop()函数。和其他的路由框架一样,pop()默认只弹出一个,如果要弹出多个,可以使用下面的方式。

//弹出到指定的路由        
context.router.popUntilRouteWithName('HomeRoute');          
//弹出到最顶部
context.router.popUntilRoot();            
复制代码

 如果要清除,或者删除路由栈里面的内容,可以是呀AutoRouter还提供了remove()函数。

context.router.removeLast();           
context.router.removeWhere((route) => );                  
复制代码

下面是AutoRouter常用方法的一个汇总。

context.pushRoute(const BooksListRoute());          
context.replaceRoute(const BooksListRoute());          
context.navigateTo(const BooksListRoute());          
context.navigateNamedTo('/books');          
context.navigateBack();         
context.popRoute();
复制代码

2.5 处理返回结果

有时候,两个路由之间,需要获取页面的处理结果,并将结果返回给上一个页面。对于这种场景,只需要在返回的时候返回结果即可,并在上一个路由使用await进行接收。

router.pop<bool>(true);    


var result = await router.push<bool>(LoginRoute()); 
复制代码

三、路由导航

3.1 嵌套导航

在应用开发中,嵌套导航是一种比较常见的场景,这意味着,在一个路由页面中嵌套另外的多个路由。

image.png

嵌套路由就像父路由的子字段一样。在上面的示例中,UsersPage、PostsPage 和SettingsPage就是DashboardPage的子路由,所以它们的定义如下。

@MaterialAutoRouter(              
  replaceInRouteName: 'Page,Route',              
  routes: <AutoRoute>[              
    AutoRoute(              
      path: '/dashboard',              
      page: DashboardPage,              
      children: [              
        AutoRoute(path: 'users', page: UsersPage),              
        AutoRoute(path: 'posts', page: PostsPage),          
        AutoRoute(path: 'settings', page: SettingsPage),                
      ],              
    ),          
    AutoRoute(path: '/login', page: LoginPage)          
  ],              
)              
class $AppRouter {}
复制代码

要完成嵌套路由渲染和构建,我们需要在嵌套路由的最外层使用AutoRouter 的小部件,如下。

class DashboardPage extends StatelessWidget {          
  @override          
  Widget build(BuildContext context) {          
    return Row(          
      children: [          
        Column(          
          children: [          
            NavLink(label: 'Users', destination: const UsersRoute()),          
            NavLink(label: 'Posts', destination: const PostsRoute()),          
            NavLink(label: 'Settings', destination: const SettingsRoute()),          
          ],          
        ),          
        Expanded(          
          // nested routes will be rendered here          
          child: AutoRouter(),          
        )          
      ],          
    );          
  }          
}
复制代码

如果我们需要跳转到嵌套路由的子组件,我们使用下面的方式就可以导航到嵌套路由的子路由。

AutoRoute(              
      path: '/dashboard',              
      page: DashboardPage,              
      children: [              
        AutoRoute(path: '', page: UsersPage),          
        //The same thing can be done using the initial flag          
        //AutoRoute(page: UsersPage,initial: true),              
        AutoRoute(path: 'posts', page: PostsPage),              
      ],              
    ),     
复制代码

3.2 Tab 导航

前面我们介绍的都是栈管理,即StackRouter,遵循先进后出的逻辑。除了支持StackRouter,auto_route还支持Tab Navigation,下面是示例代码。  

class DashboardPage extends StatelessWidget {          
  @override          
  Widget build(BuildContext context) {          
    return AutoTabsRouter(                 
      routes: const [          
        UsersRoute(),          
        PostsRoute(),          
        SettingsRoute(),          
      ],          
      builder: (context, child, animation) {                    
        final tabsRouter = AutoTabsRouter.of(context);                  
        return Scaffold(          
            body: FadeTransition(          
              opacity: animation,          
              child: child,          
            ),          
            bottomNavigationBar: BottomNavigationBar(          
              currentIndex: tabsRouter.activeIndex,          
              onTap: (index) {             
                tabsRouter.setActiveIndex(index);          
              },          
              items: [          
                BottomNavigationBarItem(label: 'Users',...),          
                BottomNavigationBarItem(label: 'Posts',...),          
                BottomNavigationBarItem(label: 'Settings',...),          
              ],          
            ));          
      },          
    );          
  }          
}
复制代码

当然,上面的代码看起来有点复杂,所以如果我们只是实现Tab导航,那么可以使用下面的简洁代码。

class DashboardPage extends StatelessWidget {          
 @override            
Widget build(context) {            
 @override          
  Widget build(context) {          
    return AutoTabsScaffold(          
       routes: const [          
        UsersRoute(),          
        PostsRoute(),          
        SettingsRoute(),          
      ],          
      bottomNavigationBuilder: (_,tabsRouter) {          
          return BottomNavigationBar(          
              currentIndex: tabsRouter.activeIndex,          
              onTap: tabsRouter.setActiveIndex          
              items: [          
                BottomNavigationBarItem(label: 'Users',...),          
                BottomNavigationBarItem(label: 'Posts',...),          
                BottomNavigationBarItem(label: 'Settings',...),          
              ],          
            )),                 
       }          
    );          
}
复制代码

3.3 PageView

当然,我们也可以使用 AutoTabsRouter.pageView 构造函数来实现使用 PageView 的选项卡。

AutoTabsRouter.pageView(      
     routes: [      
        BooksTab(),      
        ProfileTab(),      
        SettingsTab(),      
        ],     
     builder: (context, child, _) {      
        return Scaffold(      
              appBar: AppBar(      
              title: Text(context.topRoute.name),      
              leading: AutoLeadingButton()),      
              body: child,      
              bottomNavigationBar: BottomNavigationBar(          
                    currentIndex: tabsRouter.activeIndex,          
                    onTap: tabsRouter.setActiveIndex          
                    items: [          
                      BottomNavigationBarItem(label: 'Books',...),          
                      BottomNavigationBarItem(label: 'Profile',...),          
                      BottomNavigationBarItem(label: 'Settings',...),          
                    ],          
                  ),    
            ),      
      ); },    
   ); 
复制代码

3.4 声明式导航

声明式导航需要与 auto_route 一起使用,只需要使用 AutoRouter.declarative 构造函数并返回基于状态的路由列表即可。

AutoRouter.declarative(      
  routes: (handler) => [      
     BookListRoute(),      
     if(_selectedBook != null)      
     BookDetailsRoute(id: _selectedBook.id),      
 ],);
复制代码

四、高级用法

4.1 路由控制器

事实上,每个嵌套的 AutoRouter 都有自己的路由控制器来管理其内部的堆栈,获得路由控制器最简单的方法是使用上下文。在前面的示例中,我们调用的 AutoRouter.of(context) 就是用来获得根路由控制器的。

需要说明的是,对于渲染嵌套路由的 AutoRouter 小部件,我们使用上面的方式获取的 是小部件树中最近的父控制器而不是根控制器,下面是一个典型的路由控制器的结构示意图。

image.png  

从上图中可以看出,我们可以通过调用 router.parent() 来访问父路由控制器,对于这个通用函数,在真正调用的时候,我们还需要指定类型,比如StackRouter/TabsRouter。

router.parent<StackRouter>()  
router.parent<TabsRouter>()
复制代码

当然,如果是获取根路由控制器,那么是不需要进行类型转换的,因为它始终是 StackRouter。

router.root
复制代码

另一方面,为了在其他地方使用这个路由控制器,可以定义一个全局的key,比如。

class DashboardPage extends StatefulWidget {          
  @override          
  _DashboardPageState createState() => _DashboardPageState();          
}          
          
class _DashboardPageState extends State<DashboardPage> {          
  final _innerRouterKey = GlobalKey<AutoRouterState>();          
  @override          
  Widget build(BuildContext context) {          
    return Row(          
      children: [          
        Column(          
          children: [          
            NavLink(label: 'Users',          
            onTap:(){          
               final router = _innerRouterKey.currentState?.controller;          
               router?.push(const UsersRoute());          
             }          
            ),          
            ...          
          ],          
        ),          
        Expanded(          
          child: AutoRouter(key: _innerRouterKey),          
        )          
      ],          
    );          
  }          
}
复制代码

当然,我们也可以在没有全局key的情况下,使用下面的方式获取路由控制器,条件是这个路由已经启动,这个有点类似于Java的反射机制。            

context.innerRouterOf<StackRouter>(UserRoute.name)                        
context.innerRouterOf<TabsRouter>(UserRoute.name)
复制代码

4.2 Paths

在 AutoRoute 中,使用路径是可选的,因为 PageRouteInfo 对象是按名称匹配的,除非使用根委托中的 initialDeepLink、pushNamed、replaceNamed和navigateNamed 等方法。

如果我们不指定路径,系统将自动生成路径,例如BookListPage 将“book-list-page”作为路径,如果初始 arg 设置为 true,则路径将为“/”。在Flutter开发中,当页面层级比较深时,就可以使用paths方式。

AutoRoute(path: '/books', page: BookListPage),
复制代码

4.2.1 Path Parameters

当然,我们还可以在paths中添加参数。

AutoRoute(path: '/books/:id', page: BookDetailsPage),
复制代码

然后,我们只需要在目标路由使用 @PathParam('optional-alias') 方式即可获取传递的参数,比如。

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

4.2.2 Inherited Path Parameters

不过,如果使用 @PathParm() 标识的构造函数参数与路由没有同名的路径参数但它的父级有,那么该路径参数将被继承并且生成的路由不会将此作为参数。

AutoRoute(  
	  path: '/product/:id',  
	  page: ProductScreen,  
	  children: [  
		  AutoRoute(path: 'review',page: ProductReviewScreen),  
	 ],
 ),
复制代码

当然,我们还可以在路由页面添加一个名为 id 的路径参数,从上面的示例中,我们知道ProductReviewScreen没有路径参数,在这种情况下,auto_route 将检查是否有任何祖先路径可以提供此路径参数,如果有则会标记它作为路径参数,否则会引发错误。

class ProductReviewScreen extends StatelessWidget {  
  const ProductReviewScreen({super.key, @pathParam required String id}); 
}
复制代码

4.2.3 Query Parameters 

和前面的查询参数的方式相同,只需使用 @QueryParam('optional-alias') 注解构造函数参数即可获取参数的值。

RouteData.of(context).pathParams;                          
context.routeData.queryParams
复制代码

如果参数名称与路径/查询参数相同,则可以使用 const @pathParam 或者@queryParam 并且不需要传递 slug/别名,比如。

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

4.2.4 Redirecting Paths

当然,我们也可以使用RedirectRoute来实现路径的重定向,重定向路径时需要使用redirectTo参数指定重定后的路由,比如。

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

当然,使用重定向时还可以跟一些参数,比如。

<AutoRoute> [              
     RedirectRoute(path: 'books/:id', redirectTo: '/books/:id/details'),              
     AutoRoute(path: '/books/:id/details', page: BookDetailsPage),              
 ]
复制代码

除此之外,auto_route 还支持使用通配符来匹配无效或未定义的路径,可以将它作为默认的路径。

AutoRoute(path: '*', page: UnknownRoutePage)                        
AutoRoute(path: '/profile/*', page: ProfilePage)                            
RedirectRoute(path: '*', redirectTo: '/')
复制代码

4.3 路由守护

我们可以将路由守卫视为中间件或者拦截器,不经过分配的守卫无法将路由添加到堆栈中,这对于限制对某些路由的访问是很有用,相当于在执行路由跳转前我们可以对路由做一些限制。

下面,我们使用 AutoRouteGuard 创建一个路由保护,然后在 onNavigation 方法中实现我们的路由逻辑。

class AuthGuard extends AutoRouteGuard {          
 @override          
 void onNavigation(NavigationResolver resolver, StackRouter router) {          
      //触发条件          
     if(authenitcated){                 
        resolver.next(true);          
      }else{                 
         router.push(LoginRoute(onResult: (success){              
               resolver.next(success);          
          }));          
         }              
     }          
}
复制代码

在onNavigation方法中,NavigationResolver 对象包含可以调用的属性,所以我们可以使用resolver.route 访问的受保护路由,以及调用resolver.pendingRoutes 访问挂起的路由列表。

接下来,我们将守卫分配给我们想要保护的路线即可,使用方式如下。

AutoRoute(page: ProfileScreen, guards: [AuthGuard]); 
复制代码

有时候,我们希望获取父窗口包裹的小部件的上下文提供的一些值,那么只需实现 AutoRouteWrapper,并让 WrapRoute(context) 方法返回小部件的子级即可。

class ProductsScreen extends StatelessWidget implements AutoRouteWrapper {          
  @override          
  Widget wrappedRoute(BuildContext context) {          
  return Provider(create: (ctx) => ProductsBloc(), child: this);      
  }          
  ...
复制代码

4.4 路由观察者

为了方便查看路由栈的具体情况,我们可以通过扩展 AutoRouterObserver 来实现,然后重写里面的函数来进行查看,比如。

class MyObserver extends AutoRouterObserver {          
  @override          
  void didPush(Route route, Route? previousRoute) {          
    print('New route pushed: ${route.settings.name}');          
  }                  
      
 @override          
  void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) {          
    print('Tab route visited: ${route.name}');          
  } 
         
  @override          
  void didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) {          
    print('Tab route re-visited: ${route.name}');          
  }          
          
}
复制代码

然后,我们将观察者传递给根委托 AutoRouterDelegate。

return MaterialApp.router(          
      routerDelegate: AutoRouterDelegate(          
        _appRouter,          
        navigatorObservers: () => [MyObserver()],          
      ),          
      routeInformationParser: _appRouter.defaultRouteParser(),        
    );
复制代码

参考文档:pub.dev/packages/au…

猜你喜欢

转载自juejin.im/post/7129810692495900686