В предыдущей статье о навигаторе 2.0 я изложил процесс своих изысканий на тему «написать декларативный навигатор под Flutter». Получился достаточно громоздкий, но близкий к чисто декларативному, вариант. Получился? ))
Здесь я попытаюсь продвинуться дальше в разработке уже более реального (попроще) навигатора на основе «настоящего», но в урон декларативности.
«Учебный» пример https://github.com/dumptyhumpty2014gmail/navigator2gibrid
Основные изменения
Page
В первую очередь, немного упростим отдаваемые Page. В первой версии так было сделано для наглядности, а теперь создадим абстрактный класс AppPage, который будет наследоваться от Page, но дополнительно расширяться нужными нам параметрами:
abstract interface class IAppPageMixin {
AppPageUrl get pageUrl;
Map<String, String> get queryParameters;
}
abstract class AppPage extends Page implements IAppPageMixin {}
Теперь страницы можно будет создавать немного проще. К примеру, страница для авторизации:
class AppPageLogin extends AppPage {
@override
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this,
builder: (BuildContext context) {
return LoginScreen();
},
);
}
@override
AppPageUrl get pageUrl => AppPageUrl.login;
@override
Map<String, String> get queryParameters => {};
}
к содержанию ↑Configuration
Теперь мы можем сделать «универсальную» конфигурацию (состояние навигации), которая будет просто хранить в себе наши новые страницы:
class AppConfiguration {
final List<AppPage> pages;
AppConfiguration({required this.pages});
}
Потом мы здесь еще немного переработаем, но для начала достаточно.
к содержанию ↑AppRouteInformationParser
В парсере мы так же будем «ловить» конкретные строки (чтобы не допускать совсем уж свободных вариантов, типа «book/login/books/book/login», но отдавать теперь будем что-то вроде AppConfiguration(pages: [AppPageBookList()]) или AppConfiguration(pages: [AppPageBookList(), AppPageBook(id: int.parse(uri.queryParameters[‘bookid’]!))]). По сути, просто набор страниц.
к содержанию ↑AppRouterDelegate
А вот роутер-делегат поменять придется достаточно много.
В слушателе изменения авторизованы мы или нет, теперь тоже придется запускать setNewRoutePath(AppConfiguration(pages: [AppPageLogin()])); с «свободными» конфигурациями (набором страниц).
onDidRemovePage нам придется прописывать, почти как в парсере, конкретные варианты…
к содержанию ↑Переходы между страницами
При переходах на страницу (при возврате) мы теперь будем писать опять же нужный нам набор страниц, по типу setNewRoutePath(AppConfiguration(pages: [AppPageBookList(), AppPageBook(id: book.id)]))
к содержанию ↑Оптимизация
Теперь у нас есть навигатор 2.0, у которого состояние — это просто набор страниц и нам сложнее определить, где же мы находимся и тому подобное. Особенно неудобно будет в таком варианте создавать переходы на новые страницы, когда стек будет больше. К примеру, из списка книг мы перешли на страницу конкретной книги, дальше на страницу автора, потом на какую-то страницу со статистикой по автору и т.п. И нам придется писать что-то такое: setNewRoutePath(AppConfiguration(pages: [AppPageBookList(), AppPageBook(id: book.id), AppPageAuthos(id: authorId], AppPageExtraInfo())), а чтобы вернуться, снова передавать этот же список, но на одну страницу короче…
к содержанию ↑Упрощенная навигация
Идем дальше от декларативности и сделаем методы addPage и back в делегате. В простейшем виде это будет выглядеть так:
void addPage(AppPage nextPage) {
_pages = [..._currentConfiguration!.pages, nextPage];
_currentConfiguration = AppConfiguration(pages: _pages);
notifyListeners();
}
void back() {
_pages = [..._currentConfiguration!.pages.sublist(0, _currentConfiguration!.pages.length - 1)];
_currentConfiguration = AppConfiguration(pages: _pages);
notifyListeners();
}
Теперь переходы писать будет попроще.
В дальнейшем нам наверняка понадобятся такие методы, как backTo(AppPageUrl), cleare() и что-нибудь типа setFirst(AppPage), чтобы еще упростить навигацию по приложению.
к содержанию ↑Параметры
Чтобы уменьшить количество ошибок при написании парсинга параметров для страниц, предлагается наименования этих параметров хранить в статичных параметрах самой страницы:

Pop
В связи с тем, что вместо onPopPage в навигаторе теперь нужно использовать onDidRemovePage, который не управляет результатом, а всего лишь должен менять список pages, категорически, в нашем случае, нельзя применять Navigator.pop() для возврата на предыдущую страницу. Специально для этого мы сделали router.back()! Но если, в большом проекте, вы хотите «защититься» от того, чтобы какой-нибудь джун это использовал, придется вешать на каждую страницу PopScope и управлять движением назад путем установки canPop. (onPopInvokedWithResult, кстати, запускается после проверки canPop).
И еще нужно учесть такой момент, что если вы собираетесь в дальнейшем совмещать стандартные showDialog, showModalBottomSheet с нашим Навигатор 2.0, то придется или делать свой стек для них в делегате, по аналогии с страницами, или перехватывать pop в PopScope на странице. Я предпочту второй вариант, как наименее затратный и более прозрачный.
На заметку: можно еще подумать в сторону переопределения геттера popDisposition у Navigator, или пытаться таки управлять списком страниц onDidRemovePage, но страница удаляется уже после вызова этого метода, если был вызван pop().
На заметку 2: TransitionDelegate не отлавливает pop(). Проверьте сами, в проекте он есть…
к содержанию ↑Состояние авторизации
Хотя в примерах на medium сам навигатор хранит какое-то состояние (типа, currentBook), мне не нравится такая зависимость. Роутер должен быть универсальным и «вписываться» в любом новый проект.
Поэтому я изменил зависимость. Теперь мы не передаем в делегейт наш стейт и не подписываемся на изменения. Наоборот, в нашей глобальной модели-состоянии мы при изменениях состояния авторизации меняем конфигурацию навигатора.
Future<void> checkLogin() async {
_isLogin = await localRepository.getLoginState();
redirectLogin(isInit: true);
}
void changeLoginState(bool state) {
final result = localRepository.saveLoginState(state);
if (result) {
_isLogin = state;
redirectLogin();
}
}
void redirectLogin({bool isInit = false}) {
if (_isLogin == true) {
if (isInit) {
router.replace<AppPageStart>(AppConfiguration.booksList());
} else {
router.setNewRoutePath(AppConfiguration.booksList());
}
} else {
//нужно восстанавливать конфигурацию
router.setNewRoutePath(AppConfiguration.login());
}
}
В дополнение нам теперь проще сделать восстановление последнего состояния страниц в мобильных сборках. При инициализации мы будем не логин или список страниц выводить, а записанное в локальном хранилище состояние.
к содержанию ↑Android
Теперь рассмотрим, как наш навигатор будет себя вести в мобильных приложениях.
Для начала нам нужно будет изменить применение usePathUrlStrategy. Для этого мы воспользуемся вариативным импортом:
import 'stub_helpers.dart' if (dart.library.io) 'other_platform_helpers.dart' if (dart.library.js_interop) 'web_helpers.dart';
Подробности смотрите https://dart.dev/tools/pub/create-packages к данной статье эта тема не относится напрямую.
Наши addPage и back прекрасно работают. Единственный момент — нужно отдельно ловить аппаратную кнопку «назад» на андроиде или свайп на айосе. Но здесь нам поможет AppBackBtnDispatcher, в котором проверяется didPopRoute до PopScope. Или уже сам PopScope в помощь.
