Flutter Navigator 2.0 реальный пример

Обзоры > Flutter > Flutter Navigator 2.0 реальный пример

В предыдущей статье о навигаторе 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 в помощь.

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

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