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

от автора

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

Guards

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

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

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

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<AutoRoute> 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. С помощью Guards вы делаете проверку в одном месте. При любом изменении в навигации Guard автоматически проверяет статус авторизации и вызывает редирект на нужный вам маршрут.

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

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),    ],  ); }

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

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

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

Wrappers

С помощью механизма «оберток» или 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<UserEvent, String> {  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<UserBloc>().state}"),            );          },        ),      ),    );  } } 

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

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

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

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

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

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<UserBloc>().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<UserBloc> above this AboutBookScreen Widget

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

Чтобы решить эту проблему, можно использовать wrappers. В первой части мы добавили 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. Теперь Guard передаются как объект, а не как тип:

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

auto_route 7
AutoRoute(page: AuthRoute.page, guards: [AuthGuard()]),

  1. Guard больше не надо передавать в конструктор созданного класса 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>[    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<AutoRoute> 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, надеюсь, статья будет вам полезна.


ссылка на оригинал статьи https://habr.com/ru/articles/784924/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *