Русский
Русский
English
Статистика
Реклама

Navigation

Детальный разбор навигации в Flutter

24.07.2020 14:12:14 | Автор: admin

Навигация во Flutter


image


Flutter набирает популярность среди разработчиков. Большенство подходов впостроении приложений уже устоялись иприменяются ежедневно вразработке E-commerce приложений. Тема навигации опускают навторой или третий план. Какой API навигации предоставляет Фреймворк? Какие подходы выработаны? Как использовать эти подходы иначто они годятся?


Введение


Начнём с того, что такое навигация? Навигация это метод который позволяет перемещаться между пользовательским интерфейсом с заданными параметрами.
К примеру в IOS мире организовывает навигацию UIViewController, а в Android Navigation component. А чтопредоставляет Flutter?



Экраны в Flutter называются route. Для перемещениями между route существует класс Navigator который имеющий обширный API для реализации различных видов навигации.



Начнём спростого.Навигация нановый экран(route) вызывается методом push() который принимает всебя один аргумент это Route.


Navigator.push(MaterialPageRoute(builder: (BuildContext context) => MyPage()));

Давайте детальнее разберёмся ввызове метода push:


Navigator Виджет, который управляет навигацией.
Navigator.push() метод который добавляет новый route виерархию виджетов.
MaterialPageRoute() Модальный route, который заменяет весь экран адаптивным кплатформе переходом анимации.
builder обязательный аргумент конструктора MaterialPageRoute, который возвращает пользовательский интерфейс Фреймворк для отрисовки.
[MyPage](https://miro.medium.com/max/664/1Xm96KtLeIAAMtAYWcr1-MA.png)* пользовательский интерфейс реализованный при помощи Stateful/Stateless Widget


Возвращение на предыдущий route


Для возвращения с экрана на предыдущий необходимо использовать метод pop().


Navigator.pop();

Переда данных между экранами


Данные на новый экран передаются через конструктор при создании экрана в builder методе. Этот принцип работает в методах Navigator, где нужно реализовать метод build.


Navigator.push(context, MaterialPageRoute(builder: (context) => MyPage(someData: data)));

В примере продемонстрирована передача данных в класс MyPage (в этом классе хранится пользовательский интерфейс).


Для того чтобы передать данные на предыдущий экран нужно вызвать метод pop() и передать опциональным аргументом туда данные.


Navigator.pop(data);


Состояние виджета Navigator, который вызван внутри одного из видов MaterialApp/CupertinoApp/WidgetsApp. State отвечает за хранение истории навигации и предоставляет API для управления историей.
Базовые методы навигации повторяют структуру данных Stack. В диаграмме можно наблюдать методы и "call flow" NavigatorState.


http://personeltest.ru/aways/habrastorage.org/webt/5w/dg/nb/5wdgnb-tjlngub4c8y4rlpqkeqi.png


Императивный vs Декларативный подход в навигации


Во всех примерах которые были приведены выше использовался императивный подход в навигации. Разница между императивным и декларативным подходом в том как будет выглядеть вызов нового route, и кто будет хранить реализацию перехода.


Давайте на простом примере:


Императивный подход , отвечает на вопрос как?
Пример: Я вижу, что тот угловой столик свободен. Мы пойдём туда и сядем там.


Декларативный подход, отвечает на вопрос что?
Пример: Столик для двоих, пожалуйста.


Для более глубокого понимания разницы советую прочитать эту статью Imperative vs Declarative Programming


Императивная навигация


Вернёмся к реализации навигации. В императивном подходе описывается детали работы в вызывающем коде. В нашем случае это поля Route. В Flutter много типов route, например MaterialPageRoute и CupertinoPageRoute. Например в CupertinoPageRoute задаётся title, или settings.


Пример:


Navigator.push(    context,    CupertinoPageRoute<T>(        title: "Setting",        builder: (BuildContext context) => MyPage(),        settings: RouteSettings(name:"my_page"),    ),);

Этот код и знания о новом route будут хранитьсяв ViewModel/Controller/BLoC/ У этот подхода существует недостаток.


Представим что потребовалось внести изменения в конструкторе в MyPage или в CupertinoPageRoute. Нужно искать каждый вызов метода push в проекте и изменять кодовую базу.


Вывод:


Этот подход не имеет единообразный подход к навигации, и знание о реализации route проникает в бизнес логику.

Декларативная навигация


Принцип декларативной навигации заключается в использовании декларативного подхода в программировании. Давайте разберём на примере.


Пример:


Navigator.pushNamed(context, '/my_page');

Принцип императивной навигации выглядит куда проще. Говорите "Отправь пользователя на экран настроек" передавая путь одним из аргументов навигации.
Для хранении реализации роста в самом Фреймворке предусмотрен механизм у MaterialApp/CupertinoApp/WidgetsApp. Это 2 колбэка onGenerateRoute и onUnknownRoute отвеспющие за хранение деталей реализации.


Пример:


MaterialApp(    onUnknownRoute: (settings) => CupertinoPageRoute(      builder: (context) {                return UndefinedView(name: settings.name);            }    ),  onGenerateRoute: (settings) {    if (settings.name == '\my_page') {      return CupertinoPageRoute(                title: "MyPage",                settings: settings,        builder: (context) => MyPage(),      );    }        // Тут будут описание других роутов  },);

Разберёмся подробнее в реализации:
Метод onGenerateRoute данный метод срабатывает когда был вызван Navigator.pushNamed(). Метод должен вернуть route.
Метод onUnknownRoute срабатывает когда метод onGenerateRoute вернул null. должен вернуть дефолтный route, по аналогии с web сайтами 404 page.


Этот принцип упрощает вызывающий код и хранить логику переходов в одном едином месте с возможностью переиспользования кодовой базы внутри. Но так же существует недостаток, это универсальность для разных типов раутов.

Диалоговые и модальные окна


Для того чтобы вызвать диалоговое окна разного типа в Фреймворке предусмотрены глобальные методы. Давайте разберёмся в типах диалоговых окон.


Методы для вызова диалоговых и модальных окон:


  • showAboutDialog
  • showBottomSheet
  • showDatePicker
  • showGeneralDialog
  • showMenu
  • showModalBottomSheet
  • showSearch
  • showTimePicker
  • showCupertinoDialog
  • showDialog
  • showLicensePage
  • showCupertinoModalPopup

Эти методы покрывают базовые потребности разработчиков которые хотят работать с окнами. Если не хватает этого API, тогда стоит разобраться как эти методы работают.


Как работает это под капотом?


Давайте рассмотрим исходный код одного из методов, например showGeneralDialog.


Исходный код:


Future<T> showGeneralDialog<T>({  @required BuildContext context,  @required RoutePageBuilder pageBuilder,  bool barrierDismissible,  String barrierLabel,  Color barrierColor,  Duration transitionDuration,  RouteTransitionsBuilder transitionBuilder,  bool useRootNavigator = true,  RouteSettings routeSettings,}) {  assert(pageBuilder != null);  assert(useRootNavigator != null);  assert(!barrierDismissible || barrierLabel != null);  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(    pageBuilder: pageBuilder,    barrierDismissible: barrierDismissible,    barrierLabel: barrierLabel,    barrierColor: barrierColor,    transitionDuration: transitionDuration,    transitionBuilder: transitionBuilder,    settings: routeSettings,  ));}

Давайте детальнее разберёмся в устройстве этого метода. showGeneralDialog вызывает метод push у NavigatorState с _DialogRoute(). Нижнее подчёркивание обозначает что этот класс приватный и используется только в пределах области видимости в которой сам описан, то есть в пределах этого файла.


Диалоговые и модальные окна которые отображаются при помощи глобальных методов это кастомные route которые реализованы разработчиками Фреймворка.

Типы route в фреймворке


Теперь понятно что"every thins is a route", то есть что связанное с навигацией. Давайте взглянем на то, какие route уже реализованы в Фреймворке.


Два основных route в Flutter это PageRoute и PopupRoute.


PageRoute Модальный route, который заменяет весь экран.
PopupRoute Модальный route, который накладывает виджет поверх текущего route.


Реализации PageRoute:


  • MaterialPageRoute
  • CupertinoPageRoute
  • _SearchPageRoute
  • PageRouteBuilder

Реализации PopupRoute:


  • _ContexMenuRoute
  • _DialogRoute
  • _ModalBottomSheetRoute
  • _CupertinoModalPopupRoute
  • _PopupMenuRoute

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


Вывод:


Универсальный метод для декларативной навигации из капота реализовать невозможно, так как нужно учесть навигацию не только между экранами.

Best practices


В этой части статьи можно задаться вопросом, "а насколько мне всё это нужно?". Ответить на этот вопрос можно легко, если у есть желание сделать масштабируемый API навигации которая в любой момент будет модифицирована под нужды проекта, то это один из вариантов. Если приложение будет в будущем расширяться со сложной логикой.


Начнём с того что мы сделаем некий сервис который будет будет соблюдать следующим аспектам:


  • Декларативный вызов навигации.
  • Отказ от использования BuildContext для навигации (Это критично если сервис навигации будет вызываться в компонентах, в которых нет возможности получить BuildContext).
  • Модульность. Можно вызвать любой route, CupertinoPageRoute, BottomSheetRoute, DialogRoute и т.д.

Для нашего сервиса навигации нам понадобится интерфейс:


abstract class IRouter {  Future<T> routeTo<T extends Object>(RouteBundle bundle);  Future<bool> back<T extends Object>({T data, bool rootNavigator});  GlobalKey<NavigatorState> rootNavigatorKey;}

Разберём методы:
routeTo - выполняет навигацию на новый экран.
back возвращает на предыдущий экран.
rootNavigatorKey GlobalKey умеющий вызывать методы NavigatorState.

После того как мы сделали интерфейс навигации, давайте сделаем реализацию этого интерфейса.


class Router implements IRouter {    @override  GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();    @override  Future<T> routeTo<T>(RouteBundle bundle) async {   // Push logic here  }    @override  Future<bool> back<T>({T data, bool rootNavigator = false}) async {        // Back logic here    }}

Супер, теперь нам нужно реализовать метод routeTo().


@override  Future<T> routeTo<T>(RouteBundle bundle) async {        assert(bundle != null, "The bundle [RouteBundle.bundle] is null");    NavigatorState rootState = rootNavigatorKey.currentState;    assert(rootState != null, 'rootState [NavigatorState] is null');    switch (bundle.route) {      case "/routeExample":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: RouteExample(),          ),        );      case "/my_page":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: MyPage(),          ),        );      default:        throw Exception('Route is not found');    }  }

Данный метод вызывает у root NavigatorState (который описан в WidgetsApp) метод push и конфигурирует его относительно RouteBundle который приходит одним из аргументов в данный метод.


Теперь нужно реализовать класс RouteBundle. Это просто модель, которая хранит в себе набор полей для конфигурации.


enum ContainerType {  /// The parent type is [Scaffold].  ///  /// In IOS route with an iOS transition [CupertinoPageRoute].  /// In Android route with an Android transition [MaterialPageRoute].  ///  scaffold,  /// Used for show child in dialog.  ///  /// Route with [DialogRoute].  dialog,  /// Used for show child in [BottomSheet].  ///  /// Route with [ModalBottomSheetRoute].  bottomSheet,  /// Used for show child only.  /// [AppBar] and other features is not implemented.  window,}class RouteBundle {  /// Creates a bundle that can be used for [Router].  RouteBundle({    this.route,    this.containerType,  });  /// The route for current navigation.  ///  /// See [Routes] for details.  final String route;  /// The current status of this animation.  final ContainerType containerType;}

enum ContainerType тип контейнера, котрый будет задаваться декларативно из вызываемого кода.
RouteBundle класс-холдер данных отвечающих конфигурацию нового route.


Как вы могли заметить у я использовал метод _buildRoute. Именно он отвечает за то, кой тип route будет вызван.


Route<T> _buildRoute<T>({@required RouteBundle bundle, @required Widget child}) {    assert(bundle.containerType != null, "The bundle.containerType [RouteBundle.containerType] is null");    switch (bundle.containerType) {      case ContainerType.scaffold:        return CupertinoPageRoute<T>(          title: bundle.title,          builder: (BuildContext context) => child,          settings: RouteSettings(name: bundle.route),        );      case ContainerType.dialog:        return DialogRoute<T>(          title: '123',          settings: RouteSettings(name: bundle.route),          pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {                        return child;                    },        );      case ContainerType.bottomSheet:        return ModalBottomSheetRoute<T>(          settings: RouteSettings(name: bundle.route),          isScrollControlled: true,          builder: (BuildContext context) => child,        );      case ContainerType.window:        return CupertinoPageRoute<T>(          settings: RouteSettings(name: bundle.route),          builder: (BuildContext context) => child,        );      default:        throw Exception('ContainerType is not found');    }  }

Думаю что в этой функции стоит рассказать о ModalBottomSheetRoute и DialogRoute, которые использую. Исходный код этих route позаимствован из раздела Material исходного кода Flutter.


Осталось сделать метод back.


@overrideFuture<bool> back<T>({T data, bool rootNavigator = false}) async {    NavigatorState rootState = rootNavigatorKey.currentState;  return await (rootState).maybePop<T>(data);}

Ну и конечно перед использованием сервиса необходимо передать rootNavigatorKey в App следующим образом:


MaterialApp(    navigatorKey: widget.router.rootNavigatorKey,    home: Home());

Кодовая база для нашего сервиса готова, давайте вызовем наш route. Для этого создадим инстанс нашего сервиса и каким-либо образом "прокинуть" в объект, который будет вызывать этот инстанс, например при помощи Dependency Injection.



router.routeTo(RouteBundle(route: '/my_page', containerType: ContainerType.window));

Теперь мы имеем единый подход к навигации, позволяющий решить вышеперечисленные проблемы:


  • Декларативный вызов навигации
  • Отказ от BuildContext по средствам GlobalKey
  • Модульность достигнута возможностью конфигурирования route относительно имени пути и контейнера для View

Итог


В Фреймворке Flutter существуют различные методы для навигации, которые дают преимущества и недостатки.


Ну и конечно полезные ссылки:
Мой телеграм канал
Мои друзья Flutter Dev Podcast
Вакансии Flutter разработчиков

Подробнее..

Разделяй и властвуй Navigation Component в многомодульном проекте

22.01.2021 00:08:40 | Автор: admin

В этой статье вы узнаете, как можно организовать графы отдельных модулей / фич / user story, централизовать их, построить прямую навигацию между ними и присыпать сверху Safe Args плагином.

Вы сейчас в третьей части большого материала про Navigation Component в многомодульном проекте. Если не поняли ни единого слова выше, то призываю сначала ознакомиться с тем:

  • Что за зверь этот Navigation Component.

  • Как работает плагин Safe Args и что он делает.

Ну а если вы уже знакомы с этой библиотекой, то для вас есть приятный бонус в следующей статье подход к организации iOS-like multistack-навигации.

Сначала посмотрим, как выглядит разбиение проекта на модули у нас в компании, в которой я работаю (magora-systems.com):

  1. :app основной модуль и точка входа в приложение. Он должен знать обо всех модулях, участвующих в приложении.

  2. :core-модуль содержит в себе все базовые вещи: базовые классы, модели, entity, DTO, extension-ы и пр.

  3. Утилитарные модули служат для инкапсуляции функционала основных компонентов приложения. Например, работа с сетью, БД или той же навигацией.

  4. Feature-модули заключают в себе работу определенной фичи / user story, будь то флоу или экран.

Что ж, давайте натянем сову на глобус сделаем навигацию с подключенным Safe Args плагином.

Если пользоваться одним единственным графом, то будет так:

Выглядит запутанно и неочевидно. Чтобы не допустить подобного, нужно совершить ряд действий:

  1. Организовать графы для каждого feature-модуля.

  2. Выделить отдельный Top-level граф.

  3. Сделать удобные переходы между графами модулей.

  4. Решить, где хранить Top-level граф.

Теперь подробнее о каждом.

Организовать графы для каждого feature-модуля

Чтобы не было проблем с навигацией между destination-ами внутри одного модуля, сделаем отдельные графы под каждый feature-модуль и там обозначим все конечные точки и связи между ними. При таком подходе навигация фичи полностью инкапсулируется внутри модуля, а Safe Args классы, принадлежащие соответствующему графу, генерируются только тут.

Выделить отдельный Top-level граф

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

Выглядит не так эффектно, зато эффективно.

Сделать удобные переходы между графами модулей

А для того, чтобы можно было навигироваться между графами различных фич, нужно подвести к каждому global action.

Решить, где хранить Top-level граф

Тут есть несколько вариантов, у каждого есть свои плюсы и минусы:

  1. Базовый модуль (:core)

О нем знает большинство модулей, но не все. Он не знает ни об одном модуле, поэтому не сможет увидеть графы. Стоит заметить, что приложение скомпилится и будет работать несмотря на ошибки Lint-a, при мерже ресурсов все равно всё сливается воедино.

При изменении графа надо будет пересобирать проект, а так как от него зависит много модулей, то их тоже надо будет пересобрать.

  1. Главный модуль (:app)

+ Знает об абсолютно всех модулях.

О нем не знает ни один модуль.

Safe args сгенерирует global action-ы в недоступном для feature-модулей месте, поэтому мы не сможем ходить между графами.

Результаты не то чтобы очень радужные, но у нас есть целый один плюс в пользу :app-модуля, так что оставим Top-level граф в нем и займемся минусами этого подхода.Для этого нам понадобится

Минус: о нем не знает ни один модуль

Решение: сделать отдельный модуль (:navigation), о котором будут знать абсолютно все модули, которые будут хоть как-то взаимодействовать с навигацией.

Добавить в него все id глобальных action-ов. Таким образом generated-файлы поймут, с чем работают, и будут иметь доступ к id каждого глобального перехода.

<item name="actionglobalnavsignin" type="id"/><item name="actionglobalnavsignup" type="id"/><item name="actionglobalnavhome" type="id"/><item name="actionglobalnavuserslist" type="id"/><item name="actionglobalnavuserdetails" type="id"/><item name="actionglobalnavonC11CglobalC12Csettings" type="id"/><item name="actionC13Cto_faq" type="id"/>

Минус: сгенерированные Directions и Args лежат в :app модуле

Safe args сгенерирует global action-ы в недоступном для feature-модулей месте, поэтому мы не сможем ходить между графами.

Решение: перенести и доработать generated-файлы. Тут немного сложнее и придется запачкать руки о билд-скрипты. Generated-классы находятся в build-папке того модуля, где находится граф (сейчас это :app), а использовать его в :navigation-модуле неудобно. Поэтому воспользуемся костылем небольшой хитростью: во время билда дождемся конца работы таски generateSafeArgs, перекинем все созданные файлы в модуль навигации и, так как Args- и Directions-классы используют R файл модуля :app, добавим импорт нашего модуля навигации.

ext {navigationArgsPath = '/build/generated/source/navigation-args'appNavigation = "${project(':app).projectDir.path}$navigationArgsPath"navigationPath = "${project(':navigation').projectDir.path}$navigationArgsPath"navigationPackage = com.example.navigation}tasks.whenTaskAdded { task ->if (task.name.contains('generateSafeArgs')) {task.doLast {fileTree(appNavigation).filter { it.isFile() && it.name.contains("Directions") }.forEach { file ->if (file.exists()) {def lines = file.readLines()lines = lines.plus(2, "import $navigationPackage.R")file.text = lines.join("\n")}}}move(file("$appNavigation"), file("$navigationPath"))}}

В итоге

В результате мы получаем единый флоу работы, при соблюдении которого навигация в приложении декомпозируема, легко масштабируема и работает как часы. Даже если в проекте понадобится переиспользовать какую-нибудь часть графа, то её можно вынести в отдельный модуль, сделать отдельный граф и переиспользовать, как вам заблагорассудится.

Помимо всего прочего мы решили проблему с созданными плагином файлами теперь ребилд в разные флейворы и типы не будет нам докучать, как бы это было с простым добавлением sourceSet-s в модуль навигации.

На этой победе я останавливаться не стал и решил посмотреть, что еще можно с этим сделать. Для этого как никогда кстати подошел проект, в котором заказчик хотел приложение с нижним меню и чтобы каждая вкладка сохраняла свое состояние при уходе с неё. Именно о таком решении финальная часть моей истории про iOS-like multistack-навигацию.

Подробнее..

Safe Argsверный помощник Navigation Component

28.01.2021 14:21:54 | Автор: admin

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

Вы сейчас во второй части большого материала про Navigation Component в многомодульном проекте. Если вы впервые слышите про Navigation Component, то рекомендую сначала почитать, что вообще такое Navigation Component. Если уже знакомы с азами, то можно переходить к самому интересному:

Safe Argsэто плагин, идущий отдельно от Navigation Component, но созданный специально для того, чтобы с библиотекой работалось легче. С ним нет необходимости указывать id destination-а и передавать параметры через Bundleплагин генерирует для этого отдельные классы и имеет набор extension-ов для работы с ними. Давайте разбираться, как это всё работает.

Во-первых, вместе с плагином появился новый тег в xml: <argument>. Он применим и к action, и к destinationтак можно передавать и принимать параметры в более удобном виде. Во-вторых, на основе экранов и переходов, указанных в графе, генерируются специальные классы, которые можно указывать в NavController-е вместо id action.

Show me thecode!

<navigation        xmlns:android=http://schemas.android.com/apk/res/android"        xmlns:app=http://schemas.android.com/apk/res-auto"        xmlns:tools=http://schemas.android.com/tools"        android:id=@+id/graphuserflow        app:startDestination=@id/fragmentUserList>        <fragment                android:id=@+id/fragmentUserList                android:label=FragmentUserList                android:name=com.example.usersList.UserListFragment                tools:layout=@layout/fragmentuserlist>                    <action                        android:id=@+id/actiontouserdetails                        pp:destination=@id/fragmentUserList >                        <argument                                android:name=userId                                app:argType=integer                                app:nullable=false />                 </action>        </fragment>        <fragment                android:id=@+id/fragmentUserDetails                android:label=FragmentUserDetails                android:name=com.example.usersList.UserDetails                tools:layout=@layout/fragmentuser_details/></navigation>

Здесь мы взяли: достаточно простой граф, где есть фрагмент, вложенный граф и переход от одного к другому. Единственная особенностьпресловутый <argument>, с помощью которого мы передаем в users details фрагмент параметр userId. Пересоберём проект и посмотрим, что получилось.

class UserListFragmentDirections private constructor() {    private data class ActionUserFromListToDetails(    val userId: Int    ) : NavDirections {        override fun getActionId(): Int = R.id.actionToUserDetails                override fun getArguments(): Bundle {            val result = Bundle()            result.putInt(userId, this.userId)            return result        }    }    companion object {        fun actionToUserDetails(userId: Int): NavDirections =        ActionToUserDetails(userId)    }}

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

Теперь вызов перехода будет выглядеть так:

navController.navigate(    UserListFragmentDirections.actionToUserDetails(userId))

А параметры в целевом destination-е можно получить через extension, который теперь у нас имеется.

private val args by navArgs<UserDetailsFragmentArgs>()private val userId by lazy { args.userId }

Сам класс аргументов тоже генерируется в отдельный класс, который содержит методы упаковки и распаковки параметров в Bundle, но он уже не так интересен.

В итоге

Safe Argsприятное дополнение к Navigation Component, благодаря которому мы облегчили себе работу с id переходов и обработки получения/отправки их аргументов. Использовать его или нетдело ваше, но дальнейшее повествование основано на использовании это плагина. Спойлер: он принесет немало проблем, но в конце все будут счастливы:)

А теперь к самому интересному. Взглянем, как можно организовать работу с Navigation Component в многомодульном проекте совместно с SafeArgs и iOS-like multistack-навигацию.

Подробнее..

Навигация в многомодульном приложении на Jetpack без магии и DI

22.04.2021 20:09:52 | Автор: admin

Когда вы начинаете создавать приложение, в котором хотя бы несколько экранов, всегда встает вопрос - как лучше реализовать навигацию. Вопрос становится интереснее и сложнее, когда вы собираетесь делать многомодульное приложение. Примерно полтора года назад я рассказывал как можно реализовать навигацию c помощью Jetpack в многомодульном проекте. И вот спустя время, я наткнулся на свою реализацию и понял, что можно на том же Jetpack летать по модулям проще: без магии и DI.


Архитектура проекта

Чтобы покрыть основные кейсы я покажу как реализовать навигацию на многомодульном проекте такой структуры:

Типичная архитектура Android проекта: feature-модули c реализацией экранов зависят от shared-модулей с общей логикой. И app модуль, который зависит от feature и shared.

Сейчас довольно популярен подход Single Activity, поэтому в моем примере будет всего одна Activity с глобальным хостом, в котором будут переключаться фрагменты

Подготовка

От модуля shared:navigation зависят почти все модули проекта не просто так. В этом модуле реализована функция расширения фрагмента для реализации переходов.

fun Fragment.navigate(actionId: Int, hostId: Int? = null, data: Serializable? = null) {val navController = if (hostId == null) {findNavController()} else {Navigation.findNavController(requireActivity(), hostId)}val bundle = Bundle().apply { putSerializable("navigation data", data) }navController.navigate(actionId, bundle)}

У функции есть параметры:

  • actionId - id действия графа навигации

  • hostId - id хоста графа навигации. Если не будет передан, то будет использован текущий хост

  • data - объект с данными типа Serializable

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

val Fragment.navigationData: Serializable?get() = arguments?.getSerializable("navigation data")

Также в этом модуле надо описать id хостов навигации, чтобы к ним был доступ из feature модулей. Для этого в директории ресурсов надо создать файл res/value/ids.xml

<?xml version="1.0" encoding="utf-8"?><resources><item name="host_global" type="id"/><item name="host_main" type="id"/></resources>

Отлично! Подготовка завершена, можно приступать к самой реализации навигации.

Простые переходы в feature-модулях

Сэмулируем типичное поведение экрана splash. Обычно с этого экрана идет переход либо к онбордингу, либо к главному экрану приложения, либо к экрану авторизации. Реализуем нечто похожее: пусть фрагмент фичи splash будет уметь переходить на экран онбординга и на главный экран по нажатию кнопки.

Для начала создади id для этих действий: запишем их в res/value/ids.xml модуля splash

<?xml version="1.0" encoding="utf-8"?><resources><item name="action_splashFragment_to_mainFragment" type="id"/><item name="action_splashFragment_to_onboardingFragment" type="id"/></resources>

Id для действий переходов я рекомендую создавать именно в модулях фич, которые будут использовать эти действия, а не в модуле shared:navigation. Это позволяет модулю знать только о необходимых действиях.

Теперь можно использовать созданные id для выполнения переходов.

import com.example.smmn.shared.navigation.navigateclass SplashFragment : Fragment(R.layout.fragment_splash) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToOnboarding.setOnClickListener {            navigate(R.id.action_splashFragment_to_onboardingFragment)        }        buttonToMain.setOnClickListener {            navigate(R.id.action_splashFragment_to_mainFragment)        }    }}

Обратите внимание, что для выполнения перехода используется функция расширения из модуля shared:navigation.

Но чтобы этот переход заработал надо настроить глобальный хост и реализовать глобальную навигацию.

Глобальный хост

В нашей архитектуре всего одна Activity. Она будет содержать глобальный хост для фрагментов. Для этого нам ничего не потребуется реализовывать в самом коде Activity.

class MainActivity : AppCompatActivity(R.layout.activity_main)

Хост добавить надо в ее разметке activity_main.xml

<?xml version="1.0" encoding="utf-8"?><androidx.fragment.app.FragmentContainerView xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:id="@id/host_global"    android:name="androidx.navigation.fragment.NavHostFragment"    android:layout_width="match_parent"    android:layout_height="match_parent"    app:defaultNavHost="true"    app:navGraph="@navigation/navigation_global"    tools:ignore="FragmentTagUsage" />

Глобальная навигация

Это навигация, которая происходит в глобальном хосте. Для ее реализации надо реализовать в модуле app граф навигации res/navigation/navigation_global.xml

<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:id="@+id/navigation_global"    app:startDestination="@id/splashFragment">    <fragment        android:id="@+id/splashFragment"        android:name="com.example.smmn.feature.splash.SplashFragment"        android:label="SplashFragment">        <action            android:id="@id/action_splashFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />        <action            android:id="@id/action_splashFragment_to_onboardingFragment"            app:destination="@id/onboardingFragment"            app:popUpTo="@id/navigation_global" />    </fragment>    <fragment        android:id="@+id/mainFragment"        android:name="com.example.smmn.feature.main.MainFragment"        android:label="MainFragment" >        <action            android:id="@id/action_mainFragment_to_splashFragment"            app:popUpTo="@id/navigation_global"            app:destination="@id/splashFragment" />    </fragment>    <fragment        android:id="@+id/onboardingFragment"        android:name="com.example.smmn.feature.onboarding.OnboardingFragment"        android:label="OnboardingFragment">        <action            android:id="@id/action_onboardingFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />    </fragment></navigation>

Обратите внимание, что у каждого фрагмента есть набор action (действий) с помощью которых происходит переход между фрагментами. В действии указывается на какой фрагмент будет выполнен переход и как обрабатывать переход назад, например, при нажатии кнопки "Back".

И очень важно отметить, что id действий прописаны без знака +, то есть мы не создаем id в этом графе, а используем id, прописанные в feature модуле.

Прописанные id в модуле splash

<item name="action_splashFragment_to_mainFragment" type="id"/><item name="action_splashFragment_to_onboardingFragment" type="id"/>

Использование их в действиях глобального графа

        <action            android:id="@id/action_splashFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />        <action            android:id="@id/action_splashFragment_to_onboardingFragment"            app:destination="@id/onboardingFragment"            app:popUpTo="@id/navigation_global" />

Вложенный хост

В Jetpack навигации есть возможность использовать вложенный хост. Это очень полезно, когда мы хотим сделать меню типа BottomNavigation и использовать для этого меню отдельный граф навигации.

В нашем примере во вложенном хосте будут фичи профиля и настроек.

Благодаря библиотеке navigation-ui, реализовать вложенную навигацию довольно просто.

В модуле main создадим меню для BottomNavigation в res/menu/menu_main.xml

<?xml version="1.0" encoding="utf-8"?><menu xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">    <item        android:id="@+id/profileFragment"        android:icon="@drawable/ic_baseline_account_circle_24"        android:title="@string/main_menu_title_profile" />    <item        android:id="@+id/settingsFragment"        android:icon="@drawable/ic_baseline_settings_24"        android:title="@string/main_menu_title_settings" /></menu>

Создадим граф навигации в res/navigation/navigation_main.xml

<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:id="@+id/navigation_main"    app:startDestination="@id/profileFragment">    <fragment        android:id="@+id/profileFragment"        android:name="com.example.smmn.feature.profile.ProfileFragment"        android:label="ProfileFragment">        <action            android:id="@id/action_profileFragment_to_infoFragment"            app:destination="@id/infoFragment" />    </fragment>    <fragment        android:id="@+id/settingsFragment"        android:name="com.example.smmn.feature.settings.SettingsFragment"        android:label="SettingsFragment" />    <fragment        android:id="@+id/infoFragment"        android:name="com.example.smmn.feature.info.InfoFragment"        android:label="InfoFragment" /></navigation>

Здесь важно указать у фрагментов те же id что указаны в файле меню res/menu/menu_main.xml. И не забывать, что id действий брать из модулей фич.

Осталось добавить хост и меню в разметку фрагмента res/layout/fragment_main.xml

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent">    <fragment        android:id="@id/host_main"        android:name="androidx.navigation.fragment.NavHostFragment"        android:layout_width="match_parent"        android:layout_height="0dp"        app:defaultNavHost="true"        app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent"        app:navGraph="@navigation/navigation_main" />    <com.google.android.material.bottomnavigation.BottomNavigationView        android:id="@+id/bottomNavigationView"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:background="@android:color/white"        app:elevation="8dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:menu="@menu/menu_main" /></androidx.constraintlayout.widget.ConstraintLayout>

И в самом фрагменте настроить bottomNavigationView

class MainFragment : Fragment(R.layout.fragment_main) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        NavigationUI.setupWithNavController(            bottomNavigationView,            Navigation.findNavController(requireActivity(), R.id.host_main)        )    }}

Переходы между фрагментами из разных хостов

Довольно частый случай, когда надо перейти с экрана, который находится внутри вложенного хоста, на экран глобального хоста. Например, у нас есть главный экран c хостом для экранов главных фич: настроек и профиля.

И на экране настроек, который находится внутри хоста главного экрана (не глобальный хост, а глубже) надо выполнить переход на экран сплэша, который находится в глобальном хосте. Например это может понадобиться, если надо разлогинить текущего пользователя.

В этом случае также воспользуемся функцией расширения фрагмента, но укажем id глобального хоста. Мы имеем к нему доступ из фичи, так как он прописан в модуле shared:navigation.

class SettingsFragment : Fragment(R.layout.fragment_settings) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToSplash.setOnClickListener {            navigate(R.id.action_mainFragment_to_splashFragment, R.id.host_global)        }    }}

Id действия по аналогии с предыдущим переходом прописан в самом модуле фичи res/values/ids.xml

<?xml version="1.0" encoding="utf-8"?><resources><item name="action_mainFragment_to_splashFragment" type="id"/></resources>

Переходы между фрагментами с передачей и получением данных

Чтобы выполнить переход с передачей данных необходимо, чтобы данные можно было положить в bundle. Это могуг быть какие-то примитивные типы или объекты Serializable классов.

Выше я уже реализовал функцию расширения фрагмента для выполнения перехода, в которую можно передать объект Serializable класса. Аналогично вы можете реализовать передачу примитивных типов.

Чтобы передать объект Serializable класса надо чтобы модуль фичи, с которой происходит переход, и модуль фичи, на которую происходит переход, имели доступ к модулю с таким классом. В нашем случае создадим модуль shared:model где будет лежать Serializable класс Info.

data class Info(    val name: String,    val surname: String) : Serializable

Переход будет происходить с экрана profile на экран info. Создадим объект Info и передадим его в функцию расширения фрагмента.

class ProfileFragment : Fragment(R.layout.fragment_profile) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToInfo.setOnClickListener {            navigate(R.id.action_profileFragment_to_infoFragment, data = Info("name", "surname"))        }    }}

И получим данные используя другую функцию расширения фрагмента, созданную ранее.

class InfoFragment : Fragment(R.layout.fragment_info) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        val info = navigationData as? Info ?: return        textView.text = info.toString()    }}

Так это будет выглядеть в приложении

Заметьте, что мы не указывали в каком хосте выполнить переход, и переход произошел в текущем хосте.

Заключение

Таким несложным способом можно организовать навигацию в вашем многомодульном проекте, используя Jetpack и пару функций расширения. Этот подход функционально не отличается от подхода, который я описывал ранее, но в использовании он намного проще и лаконичнее.

Оставляю ссылку на код примера приложения.

Буду рад обратной связи!

Подробнее..

Основы Flutter для начинающих (Часть III)

02.06.2021 16:12:10 | Автор: admin

Поздравляю, по крайней мере, всех живущих в Сибири с наступлением лета!)))

Сегодня довольно непростая тема - навигация.

Мы рассмотрим как устроена навигация в Flutter, что вообще нужно чтобы перейти с одного экраны на другой и конечно же не забудем о передачи аргументов между экранами.

И напоследок весьма распространенный use case: создание BottomNavigationBar.

'Ну что ж не будем терять ни минуты, начинаем!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2 - файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3 (текущая статья) - BottomNavigationBar и Navigator;

  • Часть 4 - MVC. Мы будем использовать именно этот паттерн, как один из самый простых;

  • Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 - Работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 7 - Создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 8 - Немного о тестировании;

Navigator и стэк навигации

Flutter довольно прост в плане навигации, здесь нет фрагментов и Activity.

Все довольно просто: каждая страница это виджет, который называется Route.

Навигация осуществляется через объект Navigator:

// Navigator.of(context) получает состояние Navigator// виджета: NavigatorState, которое имеет push и pop методы// push помещает новую страницу на вершину стека Navigator// pop наоборот удаляет текущую страницу из вершины стэка// MaterialPageRoute в основном используется для создания// анимации между экранамиNavigator.of(context).push(MaterialPageRoute(builder: (context) => OurPage()));

Рассмотрим стэк Navigator'a на конкретном примере.

У нас есть два экрана: список книг и информация о книге.

Первый экран, который появится при запуске приложения - это список книг:

Затем мы переходим на страницу с информацией об одной из книг:

В этот момент наша новая страница находится на вершине стэка и поэтому мы не имеем доступа к списку книг.

Далее мы нажимаем кнопку Back или Up (стрелка в левом верхнем углу) и снова возвращаемся к первоначальному состоянию:

В первом случае нужно использовать push(route), во втором pop() метод.

Переходим непосредственно к практике!

Создание навигации между двумя экранами

Сделаем небольшой список персонажей из сериала My Little Pony с переходом на страницу описания каждого персонажа.

Для начала создадим новую страницу в папке pages:

Затем напишем немного кода:

import 'package:flutter/material.dart';// класс пони, который будет хранить имя и описание, а также idclass Pony {  final int id;  final String name;  final String desc;  Pony(this.id, this.name, this.desc);}// создаем список пони// final указывает на то, что мы больше// никогда не сможем присвоить имени ponies// другой список поняшекfinal List<Pony> ponies = [  Pony(      0,      "Twillight Sparkle",      "Twilight Sparkle is the central main character of My Little Pony Friendship is Magic. She is a female unicorn pony who transforms into an Alicorn and becomes a princess in Magical Mystery Cure"  ),  Pony(      1,      "Starlight Glimmer",      "Starlight Glimmer is a female unicorn pony and recurring character, initially an antagonist but later a protagonist, in the series. She first possibly appears in My Little Pony: Friends Forever Issue and first explicitly appears in the season five premiere."  ),  Pony(      2,      "Applejack",      "Applejack is a female Earth pony and one of the main characters of My Little Pony Friendship is Magic. She lives and works at Sweet Apple Acres with her grandmother Granny Smith, her older brother Big McIntosh, her younger sister Apple Bloom, and her dog Winona. She represents the element of honesty."  ),  Pony(      3,      "Pinkie Pie",      "Pinkie Pie, full name Pinkamena Diane Pie,[note 2] is a female Earth pony and one of the main characters of My Little Pony Friendship is Magic. She is an energetic and sociable baker at Sugarcube Corner, where she lives on the second floor with her toothless pet alligator Gummy, and she represents the element of laughter."  ),  Pony(      4,      "Fluttershy",      "Fluttershy is a female Pegasus pony and one of the main characters of My Little Pony Friendship is Magic. She lives in a small cottage near the Everfree Forest and takes care of animals, the most prominent of her charges being Angel the bunny. She represents the element of kindness."  ),];// PonyListPage не будет иметь состояния,// т.к. этот пример создан только для демонстрации// навигации в действииclass PonyListPage extends StatelessWidget {    // build как мы уже отметили, строит  // иерархию наших любимых виджетов  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(title: Text("Pony List Page")),      // зададим небольшие отступы для списка      body: Padding(        // объект EdgeInsets хранит четыре важные double переменные:        // left, top, right, bottom - отступ слева, сверху, справа и снизу        // EdgeInsets.all(10) - задает одинаковый отступ со всех сторон        // EdgeInsets.only(left: 10, right: 15) - задает отступ для        // определенной стороны или сторон        // EdgeInsets.symmetric - позволяет указать одинаковые        // отступы по горизонтали (left и right) и по вертикали (top и bottom)        padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),        // создаем наш список          child: ListView(            // map принимает другую функцию, которая            // будет выполняться над каждым элементом            // списка и возвращать новый элемент (виджет Material).            // Результатом map является новый список            // с новыми элементами, в данном случае            // это Material виджеты            children: ponies.map<Widget>((pony) {              // Material используется для того,              // чтобы указать цвет элементу списка              // и применить ripple эффект при нажатии на него              return Material(                color: Colors.pinkAccent,                // InkWell позволяет отслеживать                // различные события, например: нажатие                child: InkWell(                  // splashColor - цвет ripple эффекта                  splashColor: Colors.pink,                  // нажатие на элемент списка                  onTap: () {                    // добавим немного позже                  },                  // далее указываем в качестве                  // элемента Container с вложенным Text                  // Container позволяет указать внутренние (padding)                  // и внешние отступы (margin),                  // а также тень, закругление углов,                  // цвет и размеры вложенного виджета                  child: Container(                      padding: EdgeInsets.all(15),                      child: Text(                          pony.name,                          style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.white)                      )                  ),                ),              );              // map возвращает Iterable объект, который необходимо              // преобразовать в список с помощью toList() функции            }).toList(),          )      ),    );  }}

Обратите внимание, что в Dart можно определять переменные вне классов, как в нашем случае со списком.

Теперь переходим к созданию PonyDetailPage:

import 'package:flutter/material.dart';import 'pony_list_page.dart';// также, как и PonyListPage наша страница// не будет иметь состоянияclass PonyDetailPage extends StatelessWidget {  // в качестве параметра мы будет получать id пони  final int ponyId;  // конструктор PonyDetailPage принимает ponyId,  // который будет присвоен нашему ранее  // объявленному полю  PonyDetailPage(this.ponyId);  @override  Widget build(BuildContext context) {    // получаем пони по его id    // обратите внимание: мы импортируем ponies     // из файла pony_list_page.dart    final pony = ponies[ponyId];    return Scaffold(      appBar: AppBar(        title: Text("Pony Detail Page"),      ),      body: Padding(        // указываем отступ для контента        padding: EdgeInsets.all(15),        // Column размещает дочерние виджеты в виде колонки        // crossAxisAlignment - выравнивание по ширине (колонка) или        // по высоте (строка)        // mainAxisAlignment работает наоборот        // в данном случае мы растягиваем дочерние элементы        // на всю ширину колонки        child: Column(          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            Container(                padding: EdgeInsets.all(10),                // вы не можете указать color для Container,                // т.к. свойство decoration было определено                // color: Colors.pinkAccent,                                // BoxDecoration имеет дополнительные свойства,                // посравнению с Container,                // такие как: gradient, borderRadius, border, shape                // и boxShadow                // здесь мы задаем радиус закругления левого и правого                // верхних углов                decoration: BoxDecoration(                  borderRadius: BorderRadius.only(                      topLeft: Radius.circular(15),                      topRight: Radius.circular(15)                  ),         // цвет Container'а мы указываем в BoxDecoration                  color: Colors.pinkAccent,                ),                child: Text(                    // указываем имя pony                    pony.name,                    style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.white),                )            ),            Container(                padding: EdgeInsets.all(10),                child: Text(                    // указываем описание pony                    pony.desc,                    style: Theme.of(context).textTheme.bodyText1                )            )          ],        ),      )    );  }}

Осталось только организовать саму навигацию.

Добавьте следующий код в PonyListPage:

// нажатие на элемент спискаonTap: () {  // Здесь мы используем сокращенную форму:  // Navigator.of(context).push(route)  // PonyDetailPage принимает pony id,  // который мы и передалиNavigator.push(context, MaterialPageRoute(  builder: (context) => PonyDetailPage(pony.id)  ));},

Также не забудем заменить домашнюю страницу:

@override  Widget build(BuildContext context) {    // виджет MaterialApp - главный виджет приложения, который    // позволяет настроить тему и использовать    // Material Design для разработки.    return MaterialApp(      // заголовок приложения      // обычно виден, когда мы сворачиваем приложение      title: 'Json Placeholder App',      // убираем баннер      debugShowCheckedModeBanner: false,      // настройка темы, мы ещё вернёмся к этому      theme: ThemeData(        primarySwatch: Colors.blue,      ),      // теперь у нас домашная страница - PonyListPage      home: PonyListPage(),    );  }

Запуск

Теперь кликаем на любой элемент:

Та дам! Мы также можем вернуться обратно, если нажмем кнопку Back или стрелку в левом верхнем углу.

Также можно реализовать свою логику обратного перехода:

// Получаем NavigatorState и уничтожает последний элемент // из стэка навигации (PonyDetailPage)// мы можем передать второй аргумент, если хотим вернуть результатNavigator.pop(context, result)

Пока на этом все. О навигации можно написать целый цикл статей.

Ещё пара слов о нововведениях: появился новый Navigator API 2.0, о котором есть довольно хорошая статья.

Мы останавливаться не будем и переходим к BottomNavigationBar.

BottomNavigationBar и свои Navigator'ы

Я 100% уверен, что вы встречали нижнее меню, по которому можно переходить на различные экраны:

Здесь вы можете увидеть четыре элемента меню (домашная страница, поиск, уведомления и сообщения).

Давайте реализуем что-нибудь похожее.

Сначала создадим новую папку models, а в ней файл tab.dart:

Затем создадим класс Tab и перечисление TabItem:

import 'package:flutter/material.dart';// будет хранить основную информацию // об элементах менюclass MyTab {  final String name;  final MaterialColor color;  final IconData icon;  const MyTab({this.name, this.color, this.icon});}// пригодиться для определения // выбранного элемента меню// у нас будет три пункта меню и три страницы:// посты, альбомы и заданияenum TabItem { POSTS, ALBUMS, TODOS }

Переходим к более сложной части, реализации главной страницы:

import 'package:flutter/material.dart';import "../models/tab.dart";// Наша главная страница будет содержать состояниеclass HomePage extends StatefulWidget {  @override  _HomePageState createState() => _HomePageState();}class _HomePageState extends State<HomePage> {  // GlobalKey будет хранить уникальный ключ,  // по которому мы сможем получить доступ  // к виджетам, которые уже находяться в иерархии  // NavigatorState - состояние Navigator виджета  final _navigatorKeys = {    TabItem.POSTS: GlobalKey<NavigatorState>(),    TabItem.ALBUMS: GlobalKey<NavigatorState>(),    TabItem.TODOS: GlobalKey<NavigatorState>(),  };  // текущий выбранный элемент  var _currentTab = TabItem.POSTS;  // выбор элемента меню  void _selectTab(TabItem tabItem) {    setState(() => _currentTab = tabItem);  }  @override  Widget build(BuildContext context) {    // WillPopScope переопределяет поведения    // нажатия кнопки Back    return WillPopScope(      // логика обработки кнопки back может быть разной      // здесь реализована следующая логика:      // когда мы находимся на первом пункте меню (посты)      // и нажимаем кнопку Back, то сразу выходим из приложения      // в противном случае выбранный элемент меню переключается      // на предыдущий: c заданий на альбомы, с альбомов на посты,      // и после этого только выходим из приложения      onWillPop: () async {          if (_currentTab != TabItem.POSTS) {            if (_currentTab == TabItem.TODOS) {              _selectTab(TabItem.ALBUMS);            } else {              _selectTab(TabItem.POSTS);            }            return false;          } else {            return true;          }      },      child: Scaffold(        // Stack размещает один элемент над другим        // Проще говоря, каждый экран будет находится        // поверх другого, мы будем только переключаться между ними        body: Stack(children: <Widget>[          _buildOffstageNavigator(TabItem.POSTS),          _buildOffstageNavigator(TabItem.ALBUMS),          _buildOffstageNavigator(TabItem.TODOS),        ]),        // MyBottomNavigation мы создадим позже        bottomNavigationBar: MyBottomNavigation(          currentTab: _currentTab,          onSelectTab: _selectTab,        ),      ),);  }  // Создание одного из экранов - посты, альбомы или задания  Widget _buildOffstageNavigator(TabItem tabItem) {    return Offstage(      // Offstage работает следующим образом:      // если это не текущий выбранный элемент      // в нижнем меню, то мы его скрываем      offstage: _currentTab != tabItem,      // TabNavigator мы создадим позже      child: TabNavigator(        navigatorKey: _navigatorKeys[tabItem],        tabItem: tabItem,      ),    );  }}

Данной подход состоит в том, что мы создаем отдельный Navigator для постов, альбомов и заданий, у каждого из них будет свой стэк навигации.

Далее с помощью виджета Offstage мы показывает только тот экран, который был выбран.

Также мы переопределили нажатие на кнопку back - WillPopScope.

Теперь создадим нижнее меню в новом файле bottom_navigation.dart:

import 'package:flutter/material.dart';import '../models/tab.dart';// создаем три пункта меню// const обозначает, что tabs является // постоянной ссылкой и мы больше// ничего не сможем ей присвоить,// иначе говоря, она определена во время компиляцииconst Map<TabItem, MyTab> tabs = {  TabItem.POSTS : const MyTab(name: "Posts", color: Colors.red, icon: Icons.layers),  TabItem.ALBUMS : const MyTab(name: "Albums", color: Colors.blue, icon: Icons.image),  TabItem.TODOS : const MyTab(name: "Todos", color: Colors.green, icon: Icons.edit)};class MyBottomNavigation extends StatelessWidget {  // MyBottomNavigation принимает функцию onSelectTab  // и текущую выбранную вкладку  MyBottomNavigation({this.currentTab, this.onSelectTab});  final TabItem currentTab;  // ValueChanged<TabItem> - функциональный тип,  // то есть onSelectTab является ссылкой на функцию,  // которая принимает TabItem объект  final ValueChanged<TabItem> onSelectTab;  @override  Widget build(BuildContext context) {    // Используем встроенный виджет BottomNavigationBar для    // реализации нижнего меню    return BottomNavigationBar(        selectedItemColor: _colorTabMatching(currentTab),        selectedFontSize: 13,        unselectedItemColor: Colors.grey,        type: BottomNavigationBarType.fixed,        currentIndex: currentTab.index,        // пункты меню        items: [          _buildItem(TabItem.POSTS),          _buildItem(TabItem.ALBUMS),          _buildItem(TabItem.TODOS),        ],        // обработка нажатия на пункт меню        // здесь мы делаем вызов функции onSelectTab,        // которую мы получили через конструктор        onTap: (index) => onSelectTab(            TabItem.values[index]        )    );  }  // построение пункта меню  BottomNavigationBarItem _buildItem(TabItem item) {    return BottomNavigationBarItem(        // указываем иконку        icon: Icon(          _iconTabMatching(item),          color: _colorTabMatching(item),        ),        // указываем метку или название        label: tabs[item].name,    );  }  // получаем иконку элемента  IconData _iconTabMatching(TabItem item) => tabs[item].icon;  // получаем цвет элемента  Color _colorTabMatching(TabItem item) {    return currentTab == item ? tabs[item].color : Colors.grey;  }}

И реализуем TabNavigator (tab_navigator.dart):

import 'package:flutter/material.dart';import '../models/tab.dart';import 'pony_list_page.dart';class TabNavigator extends StatelessWidget {  // TabNavigator принимает:  // navigatorKey - уникальный ключ для NavigatorState  // tabItem - текущий пункт меню  TabNavigator({this.navigatorKey, this.tabItem});  final GlobalKey<NavigatorState> navigatorKey;  final TabItem tabItem;  @override  Widget build(BuildContext context) {    // наконец-то мы дошли до этого момента    // здесь мы присваиваем navigatorKey     // только, что созданному Navigator'у    // navigatorKey, как уже было отмечено является ключом,    // по которому мы получаем доступ к состоянию    // Navigator'a, вот и все!    return Navigator(      key: navigatorKey,      // Navigator имеет параметр initialRoute,      // который указывает начальную страницу и является      // всего лишь строкой.      // Мы не будем вдаваться в подробности, но отметим,      // что по умолчанию initialRoute равен /      // initialRoute: "/",            // Navigator может сам построить наши страницы или      // мы можем переопределить метод onGenerateRoute      onGenerateRoute: (routeSettings) {        // сначала определяем текущую страницу        Widget currentPage;        if (tabItem == TabItem.POSTS) {          // пока мы будем использовать PonyListPage          currentPage = PonyListPage();        } else if (tabItem == TabItem.POSTS) {          currentPage = PonyListPage();        } else {          currentPage = PonyListPage();        }        // строим Route (страница или экран)        return MaterialPageRoute(builder: (context) => currentPage,);      },    );  }}

Также не забудьте заменить домашнюю страницу в main.dart файле:

return MaterialApp(   //...   // Наша главная страница с нижнем меню   home: HomePage(),);

Осталось только импортировать нужные классы в home_page.dart и вуаля:

Также хорошей практикой является правильная организация кода, поэтому в папке pages создадим новую папку home и перетащим туда два наших файлика:

И напоследок сделаем три страницы заглушки: PostListPage, AlbumListPage и TodoListPage:

import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';// Здесь все довольно очевидноclass PostListPage extends StatefulWidget {  @override  _PostListPageState createState() => _PostListPageState();}class _PostListPageState extends State<PostListPage> {    @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post List Page"),      ),      body: Container()    );  }}

Та же структура и для двух остальных.

После этого укажим их в TabNavigator'e:

onGenerateRoute: (routeSettings) {  // сначала определяем текущую страницу  Widget currentPage;  if (tabItem == TabItem.POSTS) {  // указываем соответствующие страницы    currentPage = PostListPage();  } else if (tabItem == TabItem.ALBUMS) {    currentPage = AlbumListPage();  } else {    currentPage = TodoListPage();  }   // строим Route (страница или экран)   return MaterialPageRoute(builder: (context) => currentPage);},

Заключение

Поздравляю вас!

Искренне рад и благодарен вам за хорошие отзывы и за поддержку!

Полезные ссылки:

До скорой встречи!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru