Flutter Navigator 2.0 для web

Обзоры > Flutter > Flutter Navigator 2.0 для web

В проектах под web на Flutter изначально казалось, что достаточно будет использовать onGenerateRoute и все у нас будет нормально. А routerDelegate, который был написан для мобильных проектов, что-то плохо подружился в вебом. Количество проектов росло, проекты развивались, навигация становилась сложнее… В какой-то момент я принял решение, скрестить таки routerDelegate (вроде как, концепция Navigator 2.0) с вэбом и написать свой, относительно простой, навигатор. Главное требование — он должен хорошо работать как в мобильной версии, так и в вебе. Тем более, есть идеи сделать некоторые новые проекты так, чтобы из них собрать мобильное приложение и можно было выложить «на сайт».

Вопрос адаптивной верстки здесь оставим за скобками, а вот эксперименты, выводы и рассуждения «по ходу» создания своего нового роутерделегата постараюсь изложить здесь. И для поддержки этого творения коллегами будет потом полезно и мысли поможет структурировать. Ну и кому-то, вероятно, поможет погрузиться в тему декларативного подхода к навигации в Flutter.

«Учебный» проект на гите https://github.com/dumptyhumpty2014gmail/navigator2web

Дальнейшая проработка темы в другой статье Navigator 2.0 — реальный пример

Вводная

Задача: разработать свой навигатор со следующими свойствами:

  • должен прекрасно сочетаться с работой в браузере, то есть, ожидаемо реагировать на рефреш страницы, движения по истории (стрелки вперед, назад) и ввод ссылок (так называемых «диплинков») непосредственно в строку браузера
  • так же поддерживать диплинки в мобильной версии, переданные, к примеру, из пуша или из другого приложения через intent
  • учитывать состояние приложения, к примеру, авторизован пользователь или нет. И перенаправлять на другой экран, при необходимости
  • ссылки мы должны нормально обрабатывать разной сложности. К примеру, books/book/author?book=100&author=20 и в тоже время authors/author/book?author=20&book=100. Где books — экран со списком книг; book/12 — экран о книге с айдишником 12; authors — экран со списком авторов; author/100 — экран с данными об акторе с айдишником 100.

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

к содержанию ↑

Заметки на полях

Немного дополнительных вводных.

Постулируется, что Navigator 2.0 — это чисто декларативный подход, но везде, в том числе и в пакете go_router от команды флаттера, создаются методы типа «go», «push» и тому подобные. Что, на мой взгляд, нарушает декларативность (( Для себя я декларативность определяю так, что в чистом виде мы не должны пушить страницы и удалять, а передавать навигатору состояние страниц, которое нам нужно на данный момент. Поэтому параметр, передаваемый в setNewRoutePath — это не новая страница, которую нужно добавить, а «состояние» (конфигурация) навигации на данный момент. то есть, то, что мы желаем получить!

Но такой подход, хотя и коррелирует с общей парадигмой Flutter, как утверждается в статьях, посвященных Navigator 2.0, все таки сопряжен с бОльшим объемом кода и, в целом, сложнее. Данный проект я и попытался сделать именно декларативным. Но в целом для меня RouterDelegate — просто более гибкий и контролируемый инструмент, чем стандартный onGenerateRoute и Observers.

к содержанию ↑

Основы Navigator 2.0

Пусть это прозвучит пафосно, но здесь я в «паре абзацев» (на самом деле побольше, конечно) изложу свое виденье основ работы с концепцией Navigator 2.0.

к содержанию ↑

RouterDelegate

Чтобы убежденно говорить коллегам что-то типа «я использую Navigator 2.0», достаточно написать свой простой RouterDelegate, в котором переопределить пару методов, написать немного логики работы со страницами и подключить его или через Router или через MaterialApp.router. Наш AppRouterDelegate должен наследоваться от RouterDelegate, а так же примешиваем ему ChangeNotifier и PopNavigatorRouterDelegateMixin.

Главная задача нашего делегейта — управлять состоянием(!) навигации приложения и уведомлять Router, если состояние изменилось.

Обязательно нужно переопределить:

  • геттер navigatorKey
  @override
  final GlobalKey<NavigatorState> navigatorKey;
  • метод setNewRoutePath, который на вход получает желаемую конфигурацию стейта и производит различные манипуляции со стеком страниц, в общем случае). Код ниже не правильный (только для демонстрации метода), потому что работает не декларативно, но для случая «я использую Navigator 2.0», в простом навигаторе, можно и что-то подобное сделать ))
  @override
  Future<void> setNewRoutePath(BookRoutePath configuration) async {
    if (kDebugMode) {
      print('setNewRoutePath ${configuration.path.path}');
    }
    final page = switch (configuration.path) {
      AppRoutePaths.start => StartPage(),
      AppRoutePaths.booksList => BooksPage(books, _handleBookTapped),

      AppRoutePaths.bookDetails => BookDetailsPage(book: _selectedBook),
    };
    _currentConfiguration = configuration;
    pages.add(page);
    notifyListeners();
  }
  • ну и главный метод build, который должен вернуть Navigator, в котором выводятся наши страницы, что-то типа такого
  @override
  Widget build(BuildContext context) {
    if (kDebugMode) {
      print('pages ${pages.length}');
    }
    return Navigator(
      key: navigatorKey,

      pages: pages.isEmpty ? [StartPage()] : List.of(pages.map((e) => e)),
      onDidRemovePage: (Page route) {
        if (kDebugMode) {
          print('Route removed: ${route.toString()}');
        }
        pages.remove(route);
        notifyListeners();
      },
    );
  }

для работы с вебом нужно будет еще переопределить геттер currentConfiguration, но об этом позже

к содержанию ↑

RouteInformationParser

Это класс для работы непосредственно с вебовскими урлами. И здесь мы должны переопределить пару методов.

parseRouteInformation — метод получает из строки браузера RouteInformation, который по факту на данный момент состоит только из Uri. Мы должны написать, как этот url «превращается» в состояние приложения (навигации). Дальше это состояние (конфигурация) передается в роутер-делегейт в метод setNewRoutePath.

restoreRouteInformation — обратный, по сути, метод, который из конфигурации (состояния) навигации формирует строку браузера. Если возвращает null, строка браузера не меняется, а значит и «ломается» история.

к содержанию ↑

RootBackButtonDispatcher

Класс, который «отлавливает», по сути, нажатие кнопки «назад» в андроид-приложении (не вебовское). В рамках данной статьи не подключаем.

к содержанию ↑

RouteInformationProvider

Класс, который призван объединять работу вышеописанных классов, но… и в документации и в других статья описан мутно. В пакете go_router используется, но тоже не совсем прозрачно. Эксперименты показали, что геттер value «перебивает» RouteInformation для парсера, а если в routerReportsNewRouteInformation менять _value на новый, то входим в рекурсию. В общем, на момент написания этой статьи, не разобрался и, вроде, без этого провайдера получилось показать все «прелести» Navigator 2.0 Так что, пишите комментарии, если кто понял, как «это» применить.

к содержанию ↑

Навигация

«Соединяя» парсер и сам делегейт мы получим следующую концепцию. Если мы получаем из строки браузера Uri, то мы должны определить на его основе конфигурацию и передать ее в setNewRoutePath. А так же, непосредственно из UI (нажатием кнопки) или из слоя бизнес-логики мы запускаем setNewRoutePath с конфигурацией, которую хотим получить на данный момент. К примеру, открыт экран списка книг и открыт экран одной книги.

В setNewRoutePath из полученной конфигурации мы составляем, самое главное, List страниц и изменяем currentConfiguration, чтобы изменилась строка браузера (если мы в веб-приложении).

Если мы не хотим заморачиваться этими состояниями в UI, то можем реализовать «гибридный» вариант (не чистый декларативный). Добавим в роутер методы addPage и removePage, которые будут формировать, условно, свободное состояние.

к содержанию ↑

Реализация Navigator 2.0

Состояния

Для начала нам придется написать все возможные в нашем приложении варианты навигации. Пусть базово это будет класс IAppConfiguration, у которого мы определим метод getPages, в котором для каждого состояния будем прописывать набор нужных нам страниц в стеке. И у нас должны, для начала, быть следующие состояния:

«/» — стартовая страница, которая выводится, пока мы инициализируем приложение

«/login» — страница для логина. В учебном приложении, мы будем авторизовываться просто помещая в «хранилище» признак true.

«/books» — если мы авторизованы, то должны попадать на эту «главную» страницу нашего приложения. Здесь же мы можем сделать разавторизацию.

«/books/book?book=2» — страница конкретной книги, поверх экрана со списком книг

к содержанию ↑

Страницы

Для страниц мы сделаем класс IAppPage, у которого определим три геттера:

  • page, который возвращает непосредственно Page
  • pagePath, это строка для одного сегмента uri. Для парсера.
  • queryParameters, набор параметров для данного экрана. Тоже для парсера.

И экранов, как и конфигураций, у нас будет четыре для простоты.

Важное замечание: у Page необходимо обязательно прописывать valueKey (см. в коде), чтобы при переопределении списка страниц они не билдились с нуля каждый раз.

к содержанию ↑

AppRouteInformationParser

В методе parseRouteInformation мы будем именно парсить полученный линк и «превращать» его в состояние:

    if (uri.pathSegments.length == 2) {
      if (uri.pathSegments[1] == PagePath.book.path && uri.pathSegments[0] == PagePath.booksList.path && uri.queryParameters.containsKey('book')) {
        return BooksBookAppConfiguration(id: int.parse(uri.queryParameters['book']!));
      }
    } else if (uri.pathSegments.length == 1) {
      final segment0 = uri.pathSegments[0];
      if (segment0 == PagePath.booksList.path) {
        return BooksListAppConfiguration();
      } else if (segment0 == PagePath.login.path) {
        return LoginAppConfiguration();
      }
    }
    return StartAppConfiguration();
  }

Здесь мы четко фиксируем, какие url легитимны для нас, а для остальных будем отправлять стартовую конфигурацию.

Метод restoreRouteInformation получился более универсальным — подойдет в дальнейшем и для гибридной реализации навигатора. Мы просто от конфигурации получаем страницы и параметры для строки браузера.

к содержанию ↑

AppRouterDelegate

Здесь только «хардкор» — только метод setNewRoutePath, который принимает именно конфигурации, достает из них список страниц и заменяет его на новый. Путем замены currentConfiguration «дергает» restoreRouteInformation , чтобы изменилась строка бразуера.

Из UI переход на другую страницу будет выглядеть как декларация нового состояния. К примеру, так:

          Expanded(
            child: ListView(
              children: [
                ...books.map(
                  (book) => ListTile(
                    title: Text(book.title),
                    subtitle: Text(book.author),
                    onTap: () => {context.read<AppRouterDelegate>().setNewRoutePath(BooksBookAppConfiguration(id: book.id))},
                  ),
                ),
              ],
            ),
          ),
к содержанию ↑

mainStateListener

Чтобы отслеживать, авторизованы ли мы, вешаем слушатель на mainState. В реальных проектах это будет сделано по иному, здесь же только для демонстрации.

к содержанию ↑

Итог

По проекту раскиданы принты в ключевых местах, так что, запускайте, тестируйте различные кейсы и смотрите, как себя ведет наш навигатор.

Можно подключить AppRouteInformationProvider и поэкспериментировать с ним, в том числе.

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

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