
Flutter вышел в стабильной версии в 2018 году. Все это время он активно развивался: появилась поддержка Null safety, расширились возможности по темизации и локализации приложений, добавилось огромное количество новых виджетов. Одно из таких нововведений — Navigator 2.0, выпущенный Flutter осенью 2020 года. Это гибкий инструмент для решения непростой задачи навигации в мобильных приложениях.
Разработчики начали применять Navigator 2.0, но столкнулись с трудностями и проблемами, о которых говорили команде Flutter в официальном репозитории, предлагая упростить использование инструмента. Самым подробным материалом по новому подходу является статья в блоге Flutter, но и ее мало для того, чтобы начать работать с Navigator 2.0 в продакшен-приложениях.
С чего все началось?
Об этом расскажу я, Вова, Flutter-разработчик Центра развития финансовых технологий Россельхозбанка. Мы с командой делаем банковское мобильное приложение для юридических лиц. Это не единственное приложение банка, для создания которого используется Flutter. Фреймворк уже хорошо себя показал ранее, поэтому было принято решение использовать его и для этого проекта. Сразу обозначим результаты, к которым пришли с помощью своего варианта навигации в мобильном приложении:
Галилео. GPS Навигация 📡 GPS navigation
- реализовали 100% хотелок бизнеса;
- сократили сроки разработки по интеграции одного банковского продукта в другой;
- повысили эффективность коммуникаций между отдельными командами (кредиты, депозиты, зарплатные проекты и т.д.), разрабатывающими свои разделы в рамках отдельных репозиториев.
Дополнительно решили несколько технических задач:
- добились того, что навигация поддерживает многослойность, чтобы можно было реализовать текущие и будущие потребности бизнеса по функциональности и дизайну;
- оставили точки расширения для использования новых методов навигации в дальнейшем, потому что не знаем, какие еще возможности могут появиться в приложении. То есть спустя какое-то время, нам не придется менять технологию, на которой написан продукт;
- обеспечили гибкость: мы хотим сами выбирать подход, с помощью которого будет осуществляться навигация в мобильном приложении. Нам это нужно для поддержки разрозненных команд.
- реализовали поддержку deeplinks: приложение должно уметь открываться из сторонних источников. Например, пользователь может перейти по ссылке и у него в приложении сразу откроется страница для оплаты счета.
Мы получили опыт, о котором нигде не читали до этого. В этой статье я расскажу, какие варианты по навигации мы рассматривали и как пришли к нашему решению.
Navigator — быстрый старт
Когда в проекте мы дошли до создания навигации, то задали себе вопросы:
- Какие есть для этого возможности во Flutter?
- Какой подход выбрать?
- Какие есть подводные камни у разных вариантов?
- Нужен ли нам Navigator 2.0?
- Как все эти подходы соотносятся с нашими бизнес-требованиями?
Естественно, мы вспомнили про Navigator — самое доступное решение во Flutter для навигации, с которым прежде всего знакомятся начинающие разработчики. Navigator — это stateful-виджет, создаваемый внутри MaterialApp/CupertinoApp. State данного виджета содержит текущий стек навигации и предоставляет методы для изменения этого состояния.
web 1.0 / web 2.0 / web 3.0 ПРОСТЫМИ СЛОВАМИ
Простой переход на новую страницу выглядит следующим образом:
Navigator.push( context, MaterialPageRoute(builder: (context) => SecondPage()), );
В примере используется MaterialPageRoute, который реализует интерфейс Route. Эта сущность связывает Navigator и виджет страницы. Она определяет некоторые визуальные особенности страницы: анимацию появления и удаления, отображение на весь экран или в виде диалога, а также некоторые поведенческие особенности — например, за жест свайпа назад на iOS отвечает Route.

Кроме простого push, Navigator предоставляет обширный набор методов, которыми можно изменять стек навигации:
- push — добавление новой страницы
- pop — возврат назад (в том числе с возвратом значения)
- popUntil — возврат назад, пока не выполнится переданное условие
- pushReplacement — замена текущей страницы на другую
- pushAndRemoveUntil — добавление новой страницы и удаление из стека навигации предыдущих страниц, пока не выполнится условие.
Есть и другие методы, с которыми можно ознакомиться в официальной документации.
Можно ли расширить Navigator?
Легко заметить, что все эти методы императивные: мы говорим навигатору, как хотим изменить его состояние — добавить или удалить какой-то экран. При этом остальной фреймворк использует декларативный API для построения пользовательского интерфейса. Мы задаем некоторое дерево виджетов, на основе которого всё отображается. Не хотим сказать, что императивный подход для навигации — это плохо, наоборот, в этом нет ничего дурного — просто по-другому.
Мы обращаем ваше внимание на иное — набор методов для навигации и их поведение фиксированные: нельзя добавить свой метод или изменить реализацию имеющегося. Значит Navigator нерасширяемый — это намного важнее того, что он использует другой подход.

Например, для реализации уже не нового, но популярного подхода к навигации с bottom navigation bar придется сделать свою сущность, отвечающую за смену табов. Эта сущность будет совсем отдельно от Navigator, таким образом теряется единая точка входа в навигацию.
Navigator
На первый взгляд проблема решена, но есть и нюанс — при переходе по ссылке в стек навигации всегда будет добавлен только один экран, потому что onGenerateRoute может вернуть только один Route. Не предполагается, что deeplink может инициировать добавление сразу нескольких страниц. И ведь кейс довольно частый, например, deeplink такого вида: /users/123/post/456. Как правило, такая ссылка открывает страницу с постом и дает возможность навигации назад на страницу пользователя, который написал этот паблик. Чтобы реализовать такое поведение, придется использовать секретные техники костыль-development 🙂
Знакомимся с Router (Navigator 2.0)
Прошлой осенью разработчики Flutter выкатили новую версию компонента навигации — Navigator 2.0, который позже переименовали в Router. И это более правильное обозначение, так как Navigator 2.0 вносил путаницу. Казалось, он предназначен, чтобы заменить первый Navigator, что первая версия скоро станет deprecated и нужно срочно переезжать на новую версию. Это не так, Router — альтернативный подход, имеющийся теперь у разработчиков; он дает совершенно другие возможности.
Для начала попробуем разобраться, что такое Router. И первое, что можно найти в официальной документации — схему:

Используя эту схему, очень сложно понять, как этим пользоваться, какие компоненты нужно реализовать и что они должны делать. Да и примеров кода, как банально перейти на новый экран, сразу не видно. Попробуем разобраться.
Router — просто о сложном
Все не так уж и сложно, как кажется на первый взгляд. В новом подходе есть три основные сущности:
- Router
- RouterDelegate
- RouteInformationParser
Рассмотрим каждый из них. Router — виджет, который связывает RouterDelegate, RouteInformationParser и пользовательский интерфейс. Нам нужно реализовать RouterDelegate. Он сообщает Router о том, что изменилось состояние навигации. Когда это происходит, Router вызывает build у RouterDelegate, и пользователь видит изменение состояния приложения, то есть открывается новый экран либо происходит переход назад.
RouteInformationParser должен реализовывать два метода:
- parseRouteInformation — парсит ссылку и возвращает новое состояние навигации (в случае изменения адреса в браузере или перехода по deeplink);
- restoreRouteInformation — преобразовывает текущее состояние навигации в ссылку, которая отображается в адресной строке браузера. Его необязательно реализовывать в мобильном приложении.
Упрощенная схема взаимодействия компонентов по шагам выглядит так:

Вы спросите, а где же методы для изменения состояния навигации, как отобразить новый экран и что это за «update» и «T» на схеме? Чтобы это понять, проще всего привести аналогию со StatefulWidget. Router — это как StatefulWidget, а RouterDelegate – как State. У него также реализован build и он также сообщает об изменениях (как при setState).
Router не дает никаких методов для отображения экрана из коробки, их просто нет. Мы должны сами определить, как выглядит состояние навигации (Т на схеме) и как обновлять это состояние (update на схеме). Таким образом получаем огромную гибкость и возможность подстроить систему навигации под любые бизнес-задачи, но при этом оказываемся один на один с низкоуровневым API, без возможности сразу же начать использовать Router.
Router и полная декларативность = утопия
Разработчики Flutter предлагают использовать Router в полностью декларативном стиле, когда все состояние приложения хранится в рамках некоторого State и на основе него формируется список страниц для отображения. Примерно так:

Возьмем пример из документации, где используется такой декларативный подход, и посмотрим на него:
Код из примера от команды Flutter
Если кратко, что происходит в примере: у нас есть некоторый State навигации, состоящий из полей _selectedBook и show404. При нажатии на книгу изменяется значение _selectedBook; при переходе по deeplink происходит парсинг id книги и отображение нужной страницы либо 404.
Выглядит красиво — применили такой же подход, как при построении UI: стек навигации полностью зависит от состояния, изменяется при возникновении некоторых событий. Прямо как StatefulWidget и setState! Вау! А теперь представьте, что приложение оперирует не тремя, а десятками или сотней экранов, как это обычно бывает. Как в таком случае будет выглядеть RouterDelegate?
Как управлять всем этим состоянием? Такого примера, к сожалению, нет. И нет, команда Flutter не забила, они работают над этим вопросом, просто пока он не решен. Есть даже репозиторий на тему юзабилити компонентов фреймворка, в том числе там есть раздел про Router: https://github.com/flutter/uxr/wiki/Navigator-2.0-API-Usability-Research
Кажется, это тупик. С одной стороны, Navigator — прост в использовании, но не гибкий, с другой стороны Router — низкоуровневый, сложный, но полностью кастомизируемый. А есть что-то среднее?
Библиотеки спасают Router!
Для того, чтобы получить все лучшее от Router, при этом получить простой конечный API, нужна библиотека, которая использует Router. Это может быть свое решение, на него придется потратить много времени и сил, чтобы учесть все потребности и точки расширения, а может быть и готовое решение. Плюс готового решения в том, что его можно начать использовать также просто, как Navigator, при этом оно может иметь дополнительную функциональность от Router. Существует множество пакетов на просторах pub.dev, вот часть из них с кратким описанием особенностей каждого:
- auto_route — позволяет использовать кодогенерацию для конфигурации набора роутов, поддерживает вложенную навигацию; можно использовать как именованную навигацию через пути, так и через классы роутов;
- Beamer — предлагает интересную концепцию с разделением навигации по приложению на отдельные «разделы», у каждого из которых свой обработчик;
- Routemaster — небольшая аккуратная библиотека для навигации по URL. Можно посмотреть реализацию, чтобы понять концепцию работы с новыми компонентами;
- qlevar_router — умеет работать с многослойной навигацией, предоставляет из коробки методы для отображения диалогов и overlay;
- yeet — позволяет использовать паттерны в URL для описания параметров, предоставляет свой взгляд на вложенную навигацию;
- fluro — дает возможность использовать обработчики путей в виде функций вместо роутов;
- navigation_manager — есть поддержка uri-паттернов, используется концепция поддеревьев (sub-tree) и Duplicate strategies, как в Android Activity (в разработке).
На эти пакеты однозначно стоит взглянуть, чтобы вдохновиться их функциональными особенностями и примерами реализации.
Теперь на примере использования Routemaster рассмотрим, что нам дает такой подход:
final routes = RouteMap( routes: < ‘/’: (_) =>CupertinoTabPage( child: HomePage(), paths: [‘/feed’, ‘/settings’], ), ‘/feed’: (_) => MaterialPage(child: FeedPage()), ‘/settings’: (_) => MaterialPage(child: SettingsPage()), ‘/feed/profile/:id’: (info) => MaterialPage( child: ProfilePage(id: info.pathParameters[‘id’]) ), > ); void main() < runApp(MaterialApp.router( routerDelegate: RoutemasterDelegate(routesBuilder: (context) =>routes), routeInformationParser: RoutemasterParser(), )); >
Здесь разработчики сразу показывают пример использования многоуровневой навигации. При помощи CupertinoTabPage можно обозначить, что страница содержит вкладки, и при переходе на страницы «/feed», «/settings» фактически будет выполнена смена вкладки в рамках главного экрана:
routemaster.push(‘/feed’);
Таким образом мы абстрагируем потребителей навигации от знания о том, как именно происходит навигация — открывается ли новый экран либо просто сменяется вкладка.
На примере этого роута можно заметить поддержку Path-параметров из коробки:
‘/feed/profile/:id’: (info) => MaterialPage( child: ProfilePage(id: info.pathParameters[‘id’]) ),
Также есть поддержка редиректов, которые довольно часто используются в web:
RouteMap(routes: < ‘/one’: (routeData) =>MaterialPage(child: PageOne()), ‘/two’: (routeData) => Redirect(‘/one’), >)
При этом Routemaster старается быть похожим на подход именованной навигации из Navigator 1.0, но с дополнительной функциональностью.
В качестве примера другого подхода можем привести Beamer с его парадигмой BeamLocation. С ним становится возможным использовать декларативный подход, не сваливая в кучу состояние навигации всего приложения, как в примере от команды Flutter, а разделив его на разные BeamLocation, получив некоторые блоки, из которых строится все приложение и где каждый отвечает за свою часть.
И напоследок.
Рассмотрев все подходы к навигации, которые предоставляет Flutter, делаем вывод — нет универсального решения и каждый инструмент хорош для своей задачи:

- Navigator — нужно быстро и просто реализовать переходы между экранами;
- Router — хотим сами все контролировать и определять методы навигации, нужна полная гибкость;
- Пакет на основе Router — хотим большей функциональности, чем у Navigator, но не желаем работать с низкоуровневым API.
В своем проекте мы пришли к реализации собственной библиотеки поверх Router, чтобы получить максимальную гибкость и возможность в дальнейшем расширять ее под изменяющиеся требования бизнеса.
Отметим, что не так много банков рискуют связываться с новой технологией Flutter для создания мобильных приложений, когда еще не накоплен достаточный опыт в разработке и эксплуатации. Мы сталкивались с разными кейсами в своем проекте и удачно их решали. Если у вас есть вопросы по Flutter, пишите в комментариях — мы поделимся экспертизой!
- flutter
- кросс-платформенная разработка
- разработка мобильных приложений
- мобильная разработка
- navigator 2.0
- dart
- flutter mobile development
- navgation
- flutter navigation
- flutter router
- Блог компании Россельхозбанк
- Программирование
- Разработка мобильных приложений
- Dart
- Flutter
Источник: habr.com
Объект Navigator в JavaScript – информация о браузере

Объект navigator предназначен для предоставления подробной информации о браузере, который пользователь использует для доступа к сайту или веб-приложению. Кроме данных о браузере, в нём ещё содержится сведения о операционной системе, сетевом соединении и др.
Объект navigator – это свойство window :
const navigatorObj = window.navigator; // или без указания window // const navigatorObj = navigator;
Свойства и методы объекта navigator
Объект navigator имеет свойства и методы. Очень часто они используется для того чтобы узнать, какие функции поддерживаются браузером, а какие нет.
Свойства объекта navigator :
- appCodeName – кодовое имя браузера;
- appName – имя браузера;
- appVersion — версия браузера;
- cookieEnabled — позволяет определить включены ли cookie в браузере;
- geolocation — используется для определения местоположения пользователя;
- language — язык браузера;
- online — имеет значение true или false в зависимости от того находиться ли браузер в сети или нет;
- platform — название платформы, для которой скомпилирован браузер;
- product — имя движка браузера;
- userAgent — возвращает заголовок user agent , который браузер посылает на сервер.
Методы объекта navigator :
- javaEnabled – позволяет узнать, включён ли в браузере Java;
- sendBeacon — предназначен для отправки небольшого количества информации на веб-сервер без ожидания ответа.
Пример
Например, выведем все свойства и методы объекта Navigator на веб-страницу:
var navig = «»; for (var property in navigator) { navig += «»+property+» :» + navigator[property]; navig +=»
«; } document.getElementById(«navig»).innerHTML = navig;
Обнаружение браузера с помощью userAgent
userAgent — это строка, содержащая информацию о браузере, которую он посылает в составе заголовка запроса на сервер.
Пример содержания строки userAgent в браузере Google Chrome:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36
Она содержит сведения об операционной системе, браузере, версиях, платформах и т.д.
Эти данные можно использовать, например, для обнаружения браузера. Для этого можно написать следующую функцию:
const detectBrowser = () = { let result = ‘Other’; if (navigator.userAgent.indexOf(‘YaBrowser’) !== -1 ) { result = ‘Yandex Browser’; } else if (navigator.userAgent.indexOf(‘Firefox’) !== -1 ) { result = ‘Mozilla Firefox’; } else if (navigator.userAgent.indexOf(‘MSIE’) !== -1 ) { result = ‘Internet Exploder’; } else if (navigator.userAgent.indexOf(‘Edge’) !== -1 ) { result = ‘Microsoft Edge’; } else if (navigator.userAgent.indexOf(‘Safari’) !== -1 ) { result = ‘Safari’; } else if (navigator.userAgent.indexOf(‘Opera’) !== -1 ) { result = ‘Opera’; } else if (navigator.userAgent.indexOf(‘Chrome’) !== -1 ) { result = ‘Google Chrome’; } return result; }
Зачем это нужно? Например для того, чтобы запускать некоторые скрипты или функции только в определенном браузере.
Но при использовании navigator.userAgent следует иметь в виду, что эта информация не является 100% достоверной, поскольку она может быть изменена пользователем.
Вообще не существует 100% надежных способов идентификации браузера. Поэтому лучше проверять доступность необходимой функции или свойства, и если этой поддержки нет, то написать обходной код для реализации этого функционала или вообще его не предоставлять для этих браузеров.
Определение мобильного устройства посредством userAgent
Самый простой способ обнаружить мобильные устройства — это найти слово mobile в пользовательском агенте ( userAgent ):
const isMobile = navigator.userAgent.toLowerCase().match(/mobile/i); if (isMobile) { // для мобильных устройств } else { // для не мобильных устройств }
Более подробный вариант идентификации мобильного браузера:
const isMobile = navigator.userAgent.toLowerCase().match(/mobile/i); const isTablet = navigator.userAgent.toLowerCase().match(/tablet/i); const isAndroid = navigator.userAgent.toLowerCase().match(/android/i); const isiPhone = navigator.userAgent.toLowerCase().match(/iphone/i); const isiPad = navigator.userAgent.toLowerCase().match(/ipad/i);
Объект geolocation
Объект geolocation предназначен для определения местоположения устройства. Доступ к этому объекту осуществляется через свойство « navigator.geolocation »:
const geo = navigator.geolocation; // или так // const geo = window.navigator.geolocation;
Узнать поддерживает ли браузер геолокацию, можно посредством проверки существования свойства geolocation в объекте navigator :
if (!navigator.geolocation) { console.error(‘Ваш браузер не поддерживает геолокацию!’); }
При этом, когда сайт или веб-приложение пытается получить доступ к местонахождению пользователя, браузер из соображений конфиденциальности предоставит его только в том случае если это разрешит пользователь.
Получение текущего местоположения пользователя
Получение текущего местоположения пользователя осуществляется через метод getCurrentPosition .
// аргумент success — обязательный navigator.geolocation.getCurrentPosition(success[, error[, options]);
Этот метод посылает асинхронный запрос. В случае успеха мы можем получить местоположение устройства, в противном случае – нет.
Метод getCurrentPosition принимает 3 аргумента:
- success — функцию обратного вызова, которая будет вызвана при успешном получении геоданных (т.е. когда пользователь разрешил доступ сайту или веб-приложению к Geolocation API и данный API определил местоположение пользователя);
- error — функцию обратного вызова, которая вызывается при ошибке (т.е. когда пользователь не разрешил доступ к Geolocation API, или данный API не смог определить местонахождение пользователя, или истекло время ожидания timeout );
- options — объект, содержащий настройки.
В options можно установить:
- maximumAge — следует ли информацию о местонахождении пользователя кэшировать (в миллисекундах) или пытаться всегда получать реальное значение (значение 0 — по умолчанию);
- timeout — максимальное время в миллисекундах в течении которого нужно ждать ответ (данные о местоположении); если ответ за указанное время не пришёл, то вызывать функцию обратного вызова error (по умолчанию имеет значение infinity , т.е. ждать бесконечно);
- enableHighAccuracy — при значении true будет пытаться получить наилучшие результаты, т.е. более точное местоположение (для этого может понадобиться задействовать GPS), что в свою очередь может привести к более длительному времени отклика или увеличению энергопотребления; по умолчанию — false .
В функцию success передаётся в качестве первого аргумента объект GeolocationPosition . Он содержит информацию о местоположении устройства ( coords ) и времени, когда оно было получено ( timestamp ).
Объект coords содержит следующие свойства:
- latitude — широта (в градусах);
- longitude — долгота (в градусах);
- altitude — высота над уровнем моря (в метрах);
- speed — скорость устройства в метрах в секунду; это значение может быть null .
Пример получения местоположения устройства:
// при успешном получении сведений о местонахождении function success(position) { // position — объект GeolocationPosition, содержащий информацию о местонахождении const latitude = position.coords.latitude; const longitude = position.coords.longitude; const altitude = position.coords.altitude; const speed = position.coords.speed; // выведем значения в консоль console.log(`Широта: ${latitude}°`); console.log(`Долгота: ${longitude}°`); console.log(`Высота над уровнем моря: ${altitude}м`); console.log(`Скорость: ${speed}м/c`); } function error() { console.log(‘Произошла ошибка при определении местоположения!’); } if (!navigator.geolocation) { // получаем текущее местоположение пользователя navigator.geolocation.getCurrentPosition(success, error); }
Методы watchPosition и clearWatch
Метод watchPocation используется когда нужно получать данные о местоположении каждый раз, когда оно меняется. Метод возвращает целое число, являющееся идентификатором задачи.
navigator.geolocation.watchPosition(success[, error[, options]])
Метод clearWatch предназначен для удаления задачи по её идентификатору, которую вы создали посредством watchPosition .
// создаём задачу и сохраняем её идентификатор в watchId let watchId = navigator.geolocation.watchPosition(success, error, options); // удаляем задачу по её идентификатору clearWatch(watchId);
Источник: itchief.ru