Привет, Хабр! Меня зовут Юрий Петров, я 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. Надеюсь, вам это поможет.
-
Теперь Guard передаются как объект, а не как тип:
auto_route 5
AutoRoute(page: AuthScreen, guards: [AuthGuard]),
auto_route 7
AutoRoute(page: AuthRoute.page, guards: [AuthGuard()]),
-
Guard больше не надо передавать в конструктор созданного класса
AppRouter.
auto_route 5
class AppRouter extends _$AppRouter { AppRouter({required super.authGuard}); }
auto_route 7
class AppRouter extends _$AppRouter {}
-
Чтобы инициализировать маршрут 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), ]; }
-
В auto_route 7 нет классов
EmptyRouterPageиEmptyRouterScreen. Вместо них можно использовать созданный классAutoRouter -
Теперь класс «оберток» 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/
Добавить комментарий