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

Reactnative

Детальный разбор навигации в 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 разработчиков

Подробнее..

Как мы сделали мобильное приложение для ВкусВилл за 9 дней

08.04.2021 14:12:31 | Автор: admin
Привет, меня зовут Алексей Кафтанов, я руководитель компании FullStack (входит в ГК Автомакон). Мы занимаемся разработкой мобильных и web-приложений.

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



Проект получился классическим MVP, модель реализации подойдет для небольших b2c и почти любых b2b-проектов. Если в сжатые сроки вам требуется рабочее приложение, советую обратить внимание на этот подход. Текст будет интересен проджект-менеджерам и, скорее всего, в нем не будет ничего ранее неизвестного для программистов.

Немного истории


Эксперименты с онлайн-доставкой ВкусВилл начал проводить еще в прошлом году. Для этого была выбрана и утверждена логичная бизнес-стратегия: разработка двух разных приложений, для клиента и сборщика/курьера.

Это было удобно, клиентов было немного, загрузка приложения была небольшой: около 100 заказов в день, в процессе развития до 1000.

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

Проблемы нашей реализации


  1. Приложения были только Android. Но пандемия перетрясла все сферы, и в службы доставки стали приходить курьеры с iOS.
  2. Приложение очень долго обновлялось, например, однажды мы попали под семидневное ревью от Google. Оптимизировать продукт в таких условиях было невозможно.

От службы доставки в период изоляции зависела работа всей сети, поэтому главным вопросом, стоявшим перед командой, был: Что делать с курьерами? Тогда решили сделать telegram-бот. Потому что в боты мы умеем.

Увеличение количества заказов подтвердило успешность выбранной бизнес-модели. Но далее мы стали упираться в ограничения бота как платформы. В частности, перед разработкой стояло несколько задач:

  • Видеть маршрут и все заказы на удобной карте
  • Быть привязанным сразу к нескольким торговым точкам
  • Получать актуальную информацию о статусе заказов
  • Использовать быстрый, нативно понятный интерфейс. За время работы бот оброс большим количеством дополнительной функциональности, что значительно увеличило количество кнопок для простого, по меркам приложения, пользовательского пути.
  • Синхронизировать с приложением свою актуальную локацию. С этим пунктом у telegram наблюдаются определенные сложности и ограничения: передача локации возможна только на 8 часов.
  • Отделить пуш-уведомления внутри приложения от общих пушей в телеграмме, так курьерам удобнее реагировать именно на заказы.

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

Интересный факт: в FullStack фронтом ВкусВилла занимаются четыре героя: 2 для iOS и 2 для Android. Если вы хотите составить им компанию, напишите мне kafa@automacon.ru.

Начало разработки


В тот момент нам повезло: мы нашли ребят, рассказавших нам про no-code платформу Bubble.io. По их словам приложение по нашим запросам могло быть сделано там за неделю. Более того, они показали, как именно оно могло бы функционировать и даже прошли проверку на возможность работать с нашим довольно хитрым бекендом.

Если честно, Bubble показался мне довольно-таки сырой технологией, с точки зрения пользовательского интерфейса это несколько странная и не отзывчивая система.

Но во время знакомства с ней появилась идея: использовать принцип работы платформы для быстрого создания собственного приложения. Потому что, если с этой задачей может справиться Bubble, то почему не может, например, SPA?

Мы решили написать пользовательский интерфейс на ReactJS с использованием фреймворка Capacitor. Проект собирается в оптимизированный и сжатый набор файлов, который выгружается на удаленный сервер. Capacitor имеет доступ к нативным функциям, приложение запускается через WebView, где указан URL с собранным на ReactJS проектом. Соответственно этой логике проект должен был открываться как обычный сайт с возможностью вызова нативных функций. Удивительно, Apple без проблем пропускает подобные технологии в свой магазин приложений.

Написали, которое передали ребятам с компетенцией Bubble и одному своему программисту React. Выглядело оно довольно-таки примитивно: берем дизайн-гайд, продумываем простой UI и собираем фронт, который будет выполнять всю функциональность бота.

У каждой команды (если считать нашего программиста за команду) было 2 недели на реализацию задачи: на основе гайдлайна самостоятельно создать максимально простое и удобное приложение. Консультироваться разработчики должны были напрямую с лидером проекта со стороны бизнеса.

Подчеркну, мы сознательно отказались от проектирования, дизайна и привлечения аналитика.

Почему команд было две?


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

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

Что получилось


В первую очередь, идея параллелить команды оказалась очень логичной: интерфейсное решение no-code как-то сразу не задалось.



Поскольку задача делать по гайдлайнам стояла сразу, реализация меня как-то немного демотивировала. С точки зрения отклика у Bubble есть очевидная проблема: нажимается все топорно, часто по два раза. В процессе обнаружились еще одни танцы с бубнами: больше 2 дней у команды ушло заменить нативные для Bubble Google-карты на Яндекс. Еще 1 день сделать функционал открытия маршрутизации через 2Gis. При этом решение получилось костыльное: если 2Gis не установлен на устройство, оно все равно предлагалось. По трудозатратам у no-code команды вышло чуть больше 80 часов (первоначально именно такой лимит был установлен) при этом приложение получилось сырым. На этом сотрудничество с ними и закончили.

Решение на ReactJS получилось гораздо более оптимальным: во-первых, полный функционал получилось сделать за 67 часов, во вторых с точки зрения гайдлайнов и логики все получилось вполне рабочим:



Публикация на iOS прошла удачно: не было вопросов на ревью, уже на следующий день приложение было в сторе. Android в Play Market мы выкладывать не стали, просто разместили .apk в облачном хранилище.

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

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

Немного выводов


Удивительно, но разработка на bubble.io получилась дольше, а итоговый продукт более сырым. Существенную роль здесь сыграло ограничение конструктора.

В самом начале, при постановке ТЗ, я обратил внимание команд на важность использования гайдлайна с уже готовыми визуальными элементами: шрифтами, иконками, общим стилем и т.д. Несмотря на это у ребят из Bubble получился предельно примитивный интерфейс. Вряд ли решающую роль могло сыграть банальное не успели, скорее, дело в том, что платформа плохо кастомизируется. Если кто-то может объяснить причину, напишите в комментариях.

Многих может удивить, что мы не знали о такой методологии и ее уже повсеместно используют. Тем не менее, для меня это стало открытием и я думаю что это очень хорошее решение для корпоративных приложений с небольшим количеством Пользователей. Приложение получается в разы удобнее и принципиально отличается от адаптивной версии сайтов, сроки реализации меньше, чем при работе с ReactNative или Flutter, визуально разница не заметна.

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

Категории

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

  • Имя: Макс
    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