Управляем навигацией во Flutter с помощью библиотеки auto_route: часть 2

Привет, Хабр! Меня зовут Юрий Петров, я Flutter Team Lead в Friflex. Это продолжение моей статьи про библиотеку auto_route. В этой части рассказываю, как использовать «охранников» и «обертки», и с чем вам придется столкнуться в легаси-проектах при миграции на auto_routе_7.

52db27beed4482056c30292c4ed90fc0.jpg

«Охранники»

Представим, что у нас в приложении по управлению книгами есть алгоритм для проверки статуса авторизации пользователя в приложении.

Если пользователь прошел аутентификацию, он может просматривать детальную информацию о книге. В противном случае приложение возвращает его к аутентификации. 

Создадим экран, который будет имитировать процесс аутентификации.

auth_screen.dart

/// Для примера, реализуем фейковый, глобальный параметр
/// для проверки авторизации.
bool isAuthorized = false;

@RoutePage()
class AuthScreen extends StatelessWidget {
  const AuthScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Авторизация')),
      body: Center(
          child: ElevatedButton(
              onPressed: () {
                // При нажатии, имитируем успешное прохождение
                // аутентификации
                isAuthorized = true;

                // Переходим на главный экран
                context.pushRoute(const RootRoute());
              },
              child: const Text('Войти'))),
    );
  }
}

Логика на этом экране очень простая: нажимаем на кнопку «Войти», проводим «фейковую» аутентификацию, и когда она завершается успехом, переходим на главный экран приложения. Запускаем кодогенерацию с помощью команды:

flutter packages pub run build_runner build

Затем вновь создаем маршрут и добавляем на карту:

app_router.dart

part 'app_router.gr.dart';

@AutoRouterConfig(replaceInRouteName: 'Screen,Route')
class AppRouter extends _$AppRouter {
  @override
  List get routes => [
        /// Основной, корневой маршрут
        AutoRoute(
          page: RootRoute.page,
          initial: true,
          children: [
            /// Вложенные маршруты
            ListBooksRoutes.routes,
            AutoRoute(page: MyBooksRoute.page),
            AutoRoute(page: ProfileRoute.page),
          ],
        ),
        // Добавляем маршрут для авторизации
        AutoRoute(page: AuthRoute.page),
      ];
}

/// Пустой маршрут, нужен пока как заглушка
class EmptyRouterPage extends AutoRouter {
  const EmptyRouterPage({super.key});
}

Осталось добавить проверку на экран списка книг:

list_books_screen.dart

@RoutePage()
class ListBooksScreen extends StatelessWidget {
 const ListBooksScreen({super.key});

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Все книги'),
     ),
     body: ListView.separated(
       separatorBuilder: (context, index) {
         return const Divider(
           height: 5,
         );
       },
       itemCount: mockListBooks.length,
       itemBuilder: (context, index) {
         return ListTile(
           onTap: () {
             // Реализуем проверку,
             // пользователь авторизован или нет
             if (isAuthorized) {
               context.router.push(const AboutBookRoute());
             } else {
               context.router.push(const AuthRoute());
             }
           },
           title: Text(mockListBooks[index]),
         );
       },
     ),
   );
 }
}

Если сейчас запустить приложение, то мы увидим следующее:

демонстрация

Отлично, нас все устраивает. Но представьте, что вам нужна авторизация в разных местах приложения. И здесь уже кажется избыточным каждый раз писать условный оператор if и проверять, авторизован пользователь или нет. 

Вот как раз для этого и придумали «охранников» или guards. С помощью «охранника» вы делаете проверку в одном месте. При любом изменении в навигации «охранник» автоматически проверяет статус авторизации и вызывает редирект на нужный вам маршрут.

Делаем «охранника», который проводит авторизацию пользователя в приложении.  

auth_guard.dart

/// Создаем класс, который наследуется от AutoRouteGuard
/// и переопределяем метод onNavigation
class AuthGuard extends AutoRouteGuard {
 @override
 void onNavigation(NavigationResolver resolver, StackRouter router) {
   // Проверяем, авторизован ли пользователь
   if (isAuthorized) {
     // Если да, то переходим к экрану на который
     // пользователь хотел перейти
     resolver.next(true);
   } else {
     // Если нет, то переходим к экрану авторизации
     resolver.redirect(const AuthRoute());
   }
 }
}

Потом добавляем «охранника» в параметр guards роута AboutBookRoute.

list_books_routes.dart

abstract class ListBooksRoutes {
 static final routes = AutoRoute(
   page: ListBooksWrapperRoute.page,
   children: [
     AutoRoute(
       page: ListBooksRoute.page,
       initial: true,
     ),
     // Добавляем защитника авторизации
     // во вложенный маршрут списка книг
     AutoRoute(page: AboutBookRoute.page, guards: [AuthGuard()]),
     AutoRoute(page: SettingsBookRoute.page),
   ],
 );
}

Теперь при переходе на этот маршрут «охранник» автоматически проверит, авторизован пользователь или нет. Можно добавить «охранника» и в главный маршрут. Тогда при запуске приложения вы автоматически перейдете на экран аутентификации. Например, вот так:

app_router.dart

@AutoRouterConfig(replaceInRouteName: 'Screen,Route')
class AppRouter extends _$AppRouter {
 @override
 List get routes => [
       /// Основной, корневой маршрут
       AutoRoute(
         page: RootRoute.page,
         initial: true,
         // Добавляем глобального охранника авторизации
         // который проверяет авторизацию
         guards: [AuthGuard()],
         children: [
           /// Вложенные маршруты
           ListBooksRoutes.routes,
           AutoRoute(page: MyBooksRoute.page),
           AutoRoute(page: ProfileRoute.page),
         ],
       ),

Если у вас что-то не получилось, можно использовать этот исходный код.

«Обертки»

С помощью механизма «оберток» или wrappers можно передавать данные на группу маршрутов. Например, блоки через BlocProvider.

Возьмем блок UserBloc. Блок будет очень простым: один Event и одно состояние, которое будет хранить текст Yura.

Добавим в проект библиотеку flutter_bloc

pubspec.yaml

dependencies:
 flutter:
   sdk: flutter
 auto_route: ^7.8.4
 cupertino_icons: ^1.0.2
 flutter_bloc: ^8.1.3

Напишем блок:

user_bloc.dart

class UserBloc extends Bloc {
 UserBloc() : super('Yura');
}


final class UserEvent {}

Инициализируем UserBloc с помощью BlocProvider на экране ListBooksScreen

list_book_screen.dart

@RoutePage()
class ListBooksScreen extends StatelessWidget {
 const ListBooksScreen({super.key});


 @override
 Widget build(BuildContext context) {
   // Создаем провайдер для блока UserBloc,
   // чтобы в нем был доступ к текущему пользователю
   return BlocProvider(
     create: (context) => UserBloc(),
     child: Scaffold(
       appBar: AppBar(
         title: const Text('Все книги ]'),
       ),
       body: ListView.separated(
         separatorBuilder: (context, index) {
           return const Divider(
             height: 5,
           );
         },
         itemCount: mockListBooks.length,
         itemBuilder: (context, index) {
           return ListTile(
             onTap: () {
               context.router.push(const AboutBookRoute());
             },
             // Добавляем имя пользователя к названию книги
             title: Text(
                 "${mockListBooks[index]} ${context.read().state}"),
           );
         },
       ),
     ),
   );
 }
}

Теперь при запуске приложения, список книг выглядит так:

Список книг с именем пользователя

42a68aa602a1e37e7cea72c967d173ae.png

В своих проектах можно использовать этот исходный код.

Отлично, попробуем усложнить задачу. Представим, что нам нужно, чтобы имя пользователя выводилось при переходе на экран детальной информации о книге. 

Есть несколько вариантов, как выполнить эту задачу. Например, передать имя в конструктор маршрута или передать блок в конструктор, а потом извлечь имя из его состояния. Но если вы просто попытаетесь обратиться к блоку через контекст на экране детальной информации, например, вот так:

about_book_screen.dart

@RoutePage()
class AboutBookScreen extends StatelessWidget {
 const AboutBookScreen({super.key});


 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text(
         'О книге ${context.read().state}',
       ),
       actions: [
         IconButton(
           onPressed: () {
             context.router.push(const SettingsBookRoute());
           },
           icon: const Icon(Icons.settings),
         )
       ],
     ),
   );
 }
}

То при переходе на экран детальной информации о книге вы получите серьезную ошибку:  

Exception has occurred.ProviderNotFoundException (Error: Could not find the 
correct Provider above this AboutBookScreen Widget

Это значит, что при обращении к UserBloc он не был найден в контексте.

Чтобы решить эту проблему, можно использовать «обертки». В первой части мы добавили ListBooksWrapperScreen. Этот экран и нужен для «обертки». Чтобы у всех маршрутов был доступ кUserBloc, нужно в методе wrappedRoute инициализировать блок.

list_books_wrapper_screen.dart

@RoutePage()
class ListBooksWrapperScreen extends StatelessWidget
   implements AutoRouteWrapper {
 const ListBooksWrapperScreen({super.key});


 @override
 Widget build(BuildContext context) {
   return const EmptyRouterPage();
 }


 @override
 Widget wrappedRoute(BuildContext context) {
   // Провайдер для блока UserBloc, 
   // все вложенные маршруты получат доступ к нему
   return BlocProvider(create: (context) => UserBloc(), child: this);
 }
}

Теперь можно безопасно удалить BlocProvider из метода build экрана ListBooksScreen, так как теперь в нем нет необходимости.

Если запустить приложение, то все будет работать. UserBloc будет доступен для всех вложенных роутов.

демонстрация

Исходный код для самопроверки.

Миграция с 5 на 7 auto_route.

Ну и напоследок. Многие в проектах сталкиваются с такой проблемой, как миграция с 5 на 7 auto_route. Если вы не из их числа, то можно дальше не читать. Считайте, что вам очень повезло. 

Остальным я расскажу об основных изменениях, которые нужно внести при миграции на auto_route 7. Надеюсь, вам это поможет.

  1. Теперь «охранники» передаются как объект, а не как тип:

auto_route 5

AutoRoute(page: AuthScreen, guards: [AuthGuard]),

auto_route 7

AutoRoute(page: AuthRoute.page, guards: [AuthGuard()]),
  1. «Охранников» больше не надо передавать в конструктор созданного класса AppRouter.

auto_route 5

class AppRouter extends _$AppRouter {
 AppRouter({required super.authGuard});
}

auto_route 7

class AppRouter extends _$AppRouter {}
  1. Чтобы инициализировать маршрут  auto_route 5 сначала надо было создать карту экранов и из нее уже создавалась карта маршрута. В auto_route 7 начинаем с аннотации для экранов @RoutePage(), а после завершения кодогенерации из созданных маршрутов создаем их карту.

auto_route 5

part 'app_router.gr.dart';


@AdaptiveAutoRouter(
 replaceInRouteName: 'Screen,Route',
 routes: [
   AutoRoute(
     path: '/',
     initial: true,
     page: RootScreen,
     children: [
       AutoRoute(page: HomeScreen),
       AutoRoute(page: ProductScreen),
       FlowRoute.routers,
     ],
   ),
   AutoRoute(page: AuthScreen, guards: [AuthGuard]),
   AutoRoute(page: LoginScreen),
 ],
)
class AppRouter extends _$AppRouter {
 AppRouter({required super.authGuard});
}

auto_route 7

@RoutePage()
class AuthScreen extends StatelessWidget {
 const AuthScreen({super.key});


 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: const Text('AuthScreen')),
   );
 }
}

@AutoRouterConfig(
 replaceInRouteName: 'Screen,Route',
)
class AppRouter extends _$AppRouter {
 @override
 RouteType get defaultRouteType => const RouteType.adaptive();
 @override
 final List routes = [
   AutoRoute(path: '/', page: RootRoute.page, children: [
     AutoRoute(
       page: HomeRoute.page,
     ),
     AutoRoute(page: ProductRoute.page),
     FlowRoute.routers
   ]),
   AutoRoute(page: AuthRoute.page, guards: [AuthGuard()]),
   AutoRoute(page: LoginRoute.page),
 ];
}
  1. В auto_route 7 нет классов EmptyRouterPage и EmptyRouterScreen. Вместо  них можно использовать созданный класс  AutoRouter

  2. Теперь класс «оберток» AutoRouteWrapper необходимо имплементировать, а не миксовать.

auto_route 5

class FlowWrapperScreen extends StatelessWidget with AutoRouteWrapper

auto_route_7

@RoutePage()
class FlowWrapperScreen extends StatelessWidget 
         implements AutoRouteWrapper

Про миграцию подробнее можно посмотреть в репозитории.

На этом все. Еще раз повторюсь: я не рекомендую использовать auto_route в новых проектах. Лучше присмотритесь к go_router от команды Flutter. Но если уж встретили в проектах auto_route, надеюсь, статья будет вам полезна.

© Habrahabr.ru