Flutter auto_route路由插件详解

279 阅读9分钟

一、简介

AutoRoute 是一个强大的Flutter 路由管理插件,具有设计精简、低耦合的路由框架,支持自动生成路由代码、动态添加路由、以及路由的参数传递等功能。使用AutoRoute执行路由管理时,其基本使用流程包括:安装插件、定义路由表、运行构建、生成路由信息、以及通过 context.router 提供的 push、pop 等方法进行路由跳转。

二、基本使用

2.1 安装插件

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

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

2.2 定义路由表

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

@AutoRouterConfig()
class AppRouter extends RootStackRouter {

  @override
  RouteType get defaultRouteType => RouteType.material();

  @override
  List<AutoRoute> get routes => [
    AutoRoute(page: BookListPage, initial: true),              
    AutoRoute(page: BookDetailsPage), 
  ];
}

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

part 'router.gr.dart';

class AppRouter extends RootStackRouter {
   ... 
}

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

//自动刷新路由表
dart run build_runner watch       
//生成路由代码
dart run build_runner build

等待命令执行完成之后,即可在router.dart同级的目录下生成一个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 生成路由

在创建页面时需要使用@RoutePage()注解进行标识。

@RoutePage()
class UserViewPage extends StatefulWidget {}

然后,使用build_runner命令即可为每个声明的 AutoRoute 生成一个 PageRouteInfo 对象,这些对象包含路径信息以及从页面的默认构造函数中提取的强类型页面参数。

class UserViewRoute extends PageRouteInfo<void> {
  const UserViewRoute({List<PageRouteInfo>? children})
    : super(UserViewRoute.name, initialChildren: children);

  static const String name = 'UserViewRoute';

  static PageInfo page = PageInfo(
    name,
    builder: (data) {
      return const UserViewPage();
    },
  );
}

2.4 路由跳转

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

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

如果我们使用的是命名路由,那么可以使用NamedRoute()方法进行包裹。

router.push(NamedRoute('BookDetailsRoute', params: {'id': 1}));
router.replace(NamedRoute('BookDetailsRoute', params: {'id': 1}));
router.navigate(NamedRoute('BookDetailsRoute', params: {'id': 1}));

如果要清除或者删除路由栈里面的内容,可以使用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 嵌套导航

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

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

@AutoRouterConfig(replaceInRouteName: 'Page,Route')
class AppRouter extends RootStackRouter {

@override
List<AutoRoute> get routes => [
    AutoRoute(
      path: '/dashboard',
      page: DashboardRoute.page,
      children: [
        AutoRoute(path: 'users', page: UsersRoute.page),
        AutoRoute(path: 'posts', page: PostsRoute.page),
        AutoRoute(path: 'settings', page: SettingsRoute.page),
      ],
    ),
    AutoRoute(path: '/login', page: LoginRoute.page),
  ];
}

要完成嵌套路由渲染和构建,我们需要在嵌套路由的最外层使用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(
          child: AutoRouter(),  
        ),
      ],
    );
  }
}

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

AutoRoute(              
      path: '/dashboard',              
      page: DashboardPage,              
      children: [              
        AutoRoute(path: '', page: UsersPage),                     
        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(
      // list of your tab routes
      // routes used here must be declared as children
      // routes of /dashboard
      routes: const [
        UsersRoute(),
        PostsRoute(),
        SettingsRoute(),
      ],
      transitionBuilder: (context,child,animation) => FadeTransition(
            opacity: animation,
            // the passed child is technically our animated selected-tab page
            child: child,
          ),
      builder: (context, child) {
        // obtain the scoped TabsRouter controller using context
        final tabsRouter = AutoTabsRouter.of(context);
        // Here we're building our Scaffold inside of AutoTabsRouter
        // to access the tabsRouter controller provided in this context
        //
        // alternatively, you could use a global key
        return Scaffold(
          body: child,
          bottomNavigationBar: BottomNavigationBar(
            currentIndex: tabsRouter.activeIndex,
            onTap: (index) {
              // here we switch between tabs
              tabsRouter.setActiveIndex(index);
            },
            items: [
              BottomNavigationBarItem(label: 'Users', ...),
              BottomNavigationBarItem(label: 'Posts', ...),
              BottomNavigationBarItem(label: 'Settings', ...),
            ],
          ),
        );
      },
    );
  }
}

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

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

3.3 PageView

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

AutoTabsRouter.pageView(
  routes: [
    BooksTab(),
    ProfileTab(),
    SettingsTab(),
  ],
  builder: (context, child, _) {
    final tabsRouter = AutoTabsRouter.of(context);
    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 TabBar

使用AutoTabsRouter.tabBar还可以实现Tab导航,如下所示。

AutoTabsRouter.tabBar(
  routes: [
    BooksTab(),
    ProfileTab(),
    SettingsTab(),
  ],
  builder: (context, child, controller) {
    final tabsRouter = AutoTabsRouter.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(context.topRoute.name),
        leading: AutoLeadingButton(),
        bottom: TabBar(
          controller: controller,
          tabs: const [
            Tab(text: '1', icon: Icon(Icons.abc)),
            Tab(text: '2', icon: Icon(Icons.abc)),
            Tab(text: '3', icon: Icon(Icons.abc)),
          ],
        ),
      ),
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: tabsRouter.activeIndex,
        onTap: tabsRouter.setActiveIndex,
        items: [
          BottomNavigationBarItem(label: 'Books',...),
          BottomNavigationBarItem(label: 'Profile',...),
          BottomNavigationBarItem(label: 'Settings',...),
        ],
      ),
    );
  },
);

四、高级用法

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);

下面是一个使用示例。

class Dashboard extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Dashboard'),
        actions: [
          IconButton(
            icon: Icon(Icons.person),
            onPressed: () {
              // accessing the inner router from
              // outside the scope
              final router = context.innerRouterOf<StackRouter>(DashboardRoute.name)
              router?.push(const UsersRoute());
            },
          ),
        ],
      ),
      body: AutoRouter(), // we're trying to get access to this
    );
  }
}

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: BookListRoute.page),
]

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

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

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

AutoRoute(
  path: '*',
  page: UnknownRoute.page,
)
AutoRoute(
  path: '/profile/*',
  page: ProfileRoute.page,
)
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(),        
    );

如果您有嵌套路由器,则以下方法将不起作用,除非它们不继承观察者。

final _observer = MyObserver();
return MaterialApp.router(
  routerConfig: _appRouter.config(
    navigatorObservers: () => [_observer],
  ),
);

此时,可以将每个嵌套路由器都可以有自己的观察者并继承其父级。

AutoRouter(
  inheritNavigatorObservers: true,  
  navigatorObservers:() => [list of observers],
);

AutoTabsRouter(
  inheritNavigatorObservers: true,  
  navigatorObservers:() => [list of observers],
);