Управляем навигацией во Flutter с помощью библиотеки auto_route: часть 2
Привет, Хабр! Меня зовут Юрий Петров, я Flutter Team Lead в Friflex. Это продолжение моей статьи про библиотеку auto_route. В этой части рассказываю, как использовать «охранников» и «обертки», и с чем вам придется столкнуться в легаси-проектах при миграции на auto_routе_7.
«Охранники»
Представим, что у нас в приложении по управлению книгами есть алгоритм для проверки статуса авторизации пользователя в приложении.
Если пользователь прошел аутентификацию, он может просматривать детальную информацию о книге. В противном случае приложение возвращает его к аутентификации.
Создадим экран, который будет имитировать процесс аутентификации.
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}"),
);
},
),
),
);
}
}
Теперь при запуске приложения, список книг выглядит так:
Список книг с именем пользователя
В своих проектах можно использовать этот исходный код.
Отлично, попробуем усложнить задачу. Представим, что нам нужно, чтобы имя пользователя выводилось при переходе на экран детальной информации о книге.
Есть несколько вариантов, как выполнить эту задачу. Например, передать имя в конструктор маршрута или передать блок в конструктор, а потом извлечь имя из его состояния. Но если вы просто попытаетесь обратиться к блоку через контекст на экране детальной информации, например, вот так:
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. Надеюсь, вам это поможет.
Теперь «охранники» передаются как объект, а не как тип:
auto_route 5
AutoRoute(page: AuthScreen, guards: [AuthGuard]),
auto_route 7
AutoRoute(page: AuthRoute.page, guards: [AuthGuard()]),
«Охранников» больше не надо передавать в конструктор созданного класса
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(
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),
];
}
В 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, надеюсь, статья будет вам полезна.