Введение
Начнём с того, что такое навигация? Навигация — это механизм, который позволяет перемещаться между различными экранами внутри приложения. Во Flutter существует несколько механизмов для этого. Мы поговорим о самом простом и распространенном о классе Navigator.
Navigator
Экран во Flutter называется route. Для перемещениями между экранами существует класс Navigator
— он предоставляет методы для различных видов навигации.
Навигация на новый экран и возвращение с него
Начнём с простого. Навигация на новый экран (route) вызывается методом push()
, который принимает в себя два аргумента context
и Route
.
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => MyPage()));
Ключевым здесь является виджет MyPage
— в нем описан код страницы, которую мы открываем в качестве нового экрана. При этом мы оборачиваем виджет MyPage
при помощи класса MaterialPageRoute
(MaterialPageRoute
используется для передачи аргументов на новый экран, а также других параметров связанных с открытием экрана).
Возврат к предыдущему экрану происходит при помощи метода pop()
Navigator.pop(context);
Этот метод закрывает открытый экран, возвращая нас к предыдущему. Если вызвать этот метод на главной странице, то мы увидим чёрный экран, так как в нашем приложении не останется открытых экранов. Иными словами, стек навигации станет пустым.
Пример использования push() и pop():
Повторим еще раз. Для базовой навигации внутри приложении достаточно всего двух этих методов Navigator.push()
****для перехода на новый экран и Navigator.pop()
для возврата на предыдущий.
Давайте посмотрим на пример их использования.
В данном коде мы можем увидеть маршрутизацию между тремя экранами A, B и С. При этом, при открытии нового экрана, он добавляется в стек маршрутизации, а при закрытии удаляется из него. Более подробно эту концепцию описывает следующая картинка:
Дополнительные методы навигации
Помимо добавления и удаления экранов в стек маршрутизации, мы можем совершать со стеком более сложные действия. Например, заменить экран на вершине стека другим или удалить сразу несколько экранов.
В этом нам помогут дополнительные методы класса Navigator:
Navigator.pushNamed()
: Этот метод переходит на новый экран, используя имя маршрута из таблицы маршрутизации см. далее.Navigator.pushReplacement()
: Этот метод заменяет текущий экран на новый экран в стеке навигации.Navigator.popUntil()
: Этот метод позволяет вернуться на указанный экран в стеке навигации.- И тд. Список всех методов можно посмотреть на официальном сайте (англ.)
Таблица маршрутизации
Если наше приложение состоит из большого количества экранов, то каждый раз создавать маршрут для перехода на новый экран становится неудобно. Чтобы облегчить себе задачу, мы можем использовать таблицу маршрутизации.
Она определяет соответствие между именами маршрутов и виджетами экранов и позволяет использовать метод Navigator.pushNamed()
для перехода на экраны по их именам.
Давайте рассмотрим пример создания таблицы маршрутизации:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
initialRoute: '/A',
routes: {
'/A': (context) => PageA(),
'/B': (context) => PageB(),
'/C': (context) => PageC(),
},
);
}
}
В приведенном примере мы задали соответствующие имена маршрутов для каждого экрана. initialRoute
определяет начальный экран при запуске приложения.
Главным отличием от классического способа является использование метода pushNamed()
.
Navigator.pushNamed(context, '/routeName');
Вместо '/routeName'
нужно указать имя маршрута из таблицы: /A
, /B
или /C
.
Как видим данный метод является более лаконичным и удобным в использовании, поэтому большинство разработчиков предпочитают использовать в своих приложениях навигацию через таблицу маршрутизации.
Вот как выглядит предыдущий пример, но теперь с использованием именованных маршрутов:
Стоит отметить что при использовании таблицы маршрутизации у нас всё ещё сохраняется возможность применять Navigator.push()
, однако смешивать эти подходы к маршрутизации не стоит, так как это затруднит чтение кода.
Передача аргументов A → B
Существует несколько способов передать данные на новый экран. Мы рассмотрим два.
Первый способ
Использовать метод push()
и передать аргументы напрямую в виджет экрана.
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MyPage(data: 'Some data'))
);
При этом код самого экрана будет выглядеть так
class MyPage extends StatelessWidget {
final String data;
const MyPage({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Hello, $data'), // Используем переменную data
)
);
}
}
В этом примере мы передаем строку 'Some data'
в качестве значения аргумента data
.
Однако данный способ не подходит, если мы хотим использовать именованные маршруты. Ведь при использовании pushNamed()
у нас нет прямого доступа к виджету экрана.
Второй способ
Для передачи аргументов с помощью pushNamed()
нужно просто поместить данные в параметр arguments
.
Navigator.pushNamed(context, '/routeName', arguments: 'Some data');
Получить доступ к переданным данным можно следующим образом
final data = ModalRoute.of(context)?.settings.arguments;
Доступ к данным можно будет получить только на открывшемся экране. Как вы помните, при открытии нового экрана он оборачивается внутрь MaterialPageRoute
. Именно в нём и хранятся переданные аргументы. У каждого открытого экрана свой Route
и следовательно свой набор переданных аргументов.
ModalRoute.of(context)
позволяет получить доступ к ближайшему Route
в дереве виджетов. Иными словами, этот метод возвращает аргументы в зависимости от того, на каком экране вызывается. Если аргументы не были переданы мы получим null
.
Вот так выглядит пример использования
В данном примере мы передаём имя пользователя с экрана A на экран B.
Возврат значения A ← B
Если мы хотим вернуть определенное значение на предыдущий экран, то для этого нам необходимо сделать две вещи:
-
Указать тип возвращаемого значения при вызове метода
push()
, а также сохранить результат в переменную.final result = await Navigator.pushNamed<String?>(context, '/routeName');
-
Вернуть значение с открытого экрана при помощи метода
pop()
.Navigator.pop(context, 'Some data');
Таким образом мы сохраняем значение, переданное в pop()
, в переменную result
. Тип возвращаемых данных может быть любым String?
, int?
, bool?
и тд. По умолчанию при обычном закрытии экрана (без возврата аргументов) метод pop()
возвращает null
.
Давайте рассмотрим этот код более подробно. Что происходит когда мы открываем экран вот этой строчкой кода?
final result = await Navigator.pushNamed<String?>(context, '/routeName');
Во первых, мы останавливаем поток выполнения кода при помощи ключевого слова await
. Так как методы push()
и pushNamed()
имеют тип возвращаемого значения Future<T?>
. Иными словами, дойдя до этого места исполнение кода останавливается, пока не будет получен результат.
В данном случае результат будет получен после закрытия нового экрана. Если вам не понятен этот аспект, вы можете почитать об асинхронном программировании в в параграфе «Dart: Concurrency, изоляты».
Во вторых, мы указываем тип возвращаемого значения <String?>
. Так как методы push()
и pushNamed()
являются дженерик-методами. Узнать о дженериках можно в официальной документации (англ). Указывая тип данных <String?>
, мы просто облегчаем себе работу, так как по умолчанию возвращается тип Object?
. Если не указать тип в момент вызова, то результат с типом Object?
нужно будет преобразовывать к String?
вручную. Однако важно помнить, что если вы работаете со строгой типизацией в методе pushNamed
, этим же типом должен быть помечен MaterialPageRoute
, соответствующий открываемой странице.
Однако нужно помнить, что возвращаемое значение может быть null
(например если пользователь закрыл экран нажатием кнопки «назад»). Поэтому тип должен быть обнуляемым (nullable). Также не забудьте добавить проверку на null
, прежде чем использовать полученное значение.
Давайте посмотрим на пример кода
В данном примере мы возвращаем имя пользователя с экрана B на экран A.
Заключение
Navigation 1.0 предоставляет мощные инструменты для управления навигацией в приложениях Flutter.
Мы рассмотрели основы навигации с использованием навигационного стека и методов Navigator.push()
и Navigator.pop()
. А кроме того — научились создавать удобные переходы на экраны по их именам с помощью таблицы маршрутизации и передавать аргументы между экранами.