2.20. Project: основы навигации

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

Во Flutter есть несколько механизмов для навигации, но мы рассмотрим самый прострой и распространённый — с помощью класса Navigator. Этот механизм называется Navigation 1.0.

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

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

Ключевым здесь является виджет MyPage — в нем описан код страницы, которую мы открываем в качестве нового экрана. При этом мы оборачиваем виджет MyPage при помощи класса MaterialPageRoute (MaterialPageRoute используется для передачи аргументов на новый экран, а также других параметров связанных с открытием экрана).

Возврат к предыдущему экрану происходит при помощи метода pop()

1Navigator.pop(context);

Этот метод закрывает открытый экран, возвращая нас к предыдущему. Если вызвать этот метод на главной странице, то мы увидим чёрный экран, так как в нашем приложении не останется открытых экранов. Иными словами, стек навигации станет пустым.

Пример использования push() и pop()

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

Давайте посмотрим на пример их использования.

В коде ниже мы можем увидеть маршрутизацию между тремя экранами A, B и С. При этом, при открытии нового экрана, он добавляется в стек маршрутизации, а при закрытии удаляется из него.

Более подробно эту концепцию описывает следующая картинка:

fluttern

Дополнительные методы навигации

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

В этом нам помогут дополнительные методы класса Navigator:

  • Navigator.pushNamed() — переходит на новый экран, используя имя маршрута из таблицы маршрутизации (о ней расскажем ниже).
  • Navigator.pushReplacement() — заменяет текущий экран на новый экран в стеке навигации.
  • Navigator.popUntil() — позволяет вернуться на указанный экран в стеке навигации.

Список всех методов можно посмотреть в документации.

Таблица маршрутизации

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

Она определяет соответствие между именами маршрутов и виджетами экранов и позволяет использовать метод Navigator.pushNamed() для перехода на экраны по их именам.

Пример
1class MyApp extends StatelessWidget {
2  @override
3  Widget build(BuildContext context) {
4    return MaterialApp(
5      title: 'My App',
6      initialRoute: '/A',
7      routes: {
8        '/A': (context) => PageA(),
9        '/B': (context) => PageB(),
10        '/C': (context) => PageC(),
11      },
12    );
13  }
14}

В приведённом примере мы задали соответствующие имена маршрутов для каждого экрана. initialRoute определяет начальный экран при запуске приложения.

Главное отличие от классического способа — использование метода pushNamed().

1Navigator.pushNamed(context, '/routeName');

Вместо '/routeName' нужно указать имя маршрута из таблицы: /A/B или /C.

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

Вот как выглядит предыдущий пример, но теперь с использованием именованных маршрутов:

Стоит отметить что при использовании таблицы маршрутизации у нас всё ещё сохраняется возможность применять Navigator.push(), однако смешивать эти подходы к маршрутизации не стоит, так как это затруднит чтение кода.

Передача данных на новый экран

Существует несколько способов передать данные на новый экран. Мы рассмотрим два:

  • с помощью метода push();
  • с помощью метода pushNamed();

Первый способ

Используем метод push() и передадим аргументы напрямую в виджет экрана.

1Navigator.push(
2  context, 
3  MaterialPageRoute(builder: (context) => MyPage(data: 'Some data'))
4);
При этом код самого экрана будет выглядеть так
1class MyPage extends StatelessWidget {
2  final String data;
3
4  const MyPage({super.key, required this.data});
5
6  @override
7  Widget build(BuildContext context) {
8    return Scaffold(
9      body: Center(
10        child: Text('Hello, $data'), // Используем переменную data
11      )
12    );
13  }
14}

В этом примере мы передаем строку 'Some data' в качестве значения аргумента data.

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

Второй способ

Для передачи аргументов с помощью pushNamed() нужно просто поместить данные в параметр arguments.

1Navigator.pushNamed(context, '/routeName', arguments: 'Some data');

Получить доступ к переданным данным можно следующим образом:

1final data = ModalRoute.of(context)?.settings.arguments;

Доступ к данным можно будет получить только на открывшемся экране. Как вы помните, при открытии нового экрана он оборачивается внутрь MaterialPageRoute. Именно в нём и хранятся переданные аргументы. У каждого открытого экрана свой Route и следовательно свой набор переданных аргументов.

ModalRoute.of(context) позволяет получить доступ к ближайшему Route в дереве виджетов. Иными словами, этот метод возвращает аргументы в зависимости от того, на каком экране вызывается. Если аргументы не были переданы мы получим null.

Вот как это выглядит:

В данном примере мы передаём имя пользователя с экрана A на экран B. Вот как это выглядит в коде:

В данном примере мы передаём имя пользователя с экрана A на экран B.

Возврат данных на предыдущий экран

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

  1. Указать тип возвращаемого значения при вызове метода push(), а также сохранить результат в переменную.

    1final result = await Navigator.pushNamed<String?>(context, '/routeName');
    
  2. Вернуть значение с открытого экрана при помощи метода pop().

    1Navigator.pop(context, 'Some data');
    

Таким образом мы сохраняем значение, переданное в pop(), в переменную result. Тип возвращаемых данных может быть любым String?int?bool? и так далее. По умолчанию при обычном закрытии экрана (без возврата аргументов) метод pop() возвращает null.

Давайте рассмотрим этот код более подробно.

1final result = await Navigator.pushNamed<String?>(context, '/routeName');

Когда мы открываем экран вот этой строчкой кода, происходит две вещи.

Во первых, мы останавливаем поток выполнения кода при помощи ключевого слова await. Так как методы push() и pushNamed() имеют тип возвращаемого значения Future<T?>. Иными словами, дойдя до этого места исполнение кода останавливается, пока не будет получен результат. В данном случае результат будет получен после закрытия нового экрана.

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

Во вторых, мы указываем тип возвращаемого значения <String?>. Так как методы push() и pushNamed() — это дженерик-методы. Указывая тип данных <String?>, мы просто облегчаем себе работу, так как по умолчанию возвращается тип Object?. Если не указать тип в момент вызова, то результат с типом Object? нужно будет преобразовывать к String? вручную.

Однако важно помнить, что если вы работаете со строгой типизацией в методе pushNamed, этим же типом должен быть помечен MaterialPageRoute, который соответствует открываемой странице.

Однако нужно помнить, что возвращаемое значение может быть null (например если пользователь закрыл экран нажатием кнопки «назад»). Поэтому тип должен быть обнуляемым (nullable). Также не забудьте добавить проверку на null, прежде чем использовать полученное значение.

Вот как можно вернуть пользователя с экрана B на экран A:

Вот код для этого примера:

C навигацией разобрались, давайте коротко вспомним, что мы узнали.

Мы рассмотрели основы навигации с использованием навигационного стека и методов Navigator.push() и Navigator.pop(). А кроме того — научились создавать удобные переходы на экраны по их именам с помощью таблицы маршрутизации и передавать аргументы между экранами.

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

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.19. Project: темизация
Следующий параграф2.21. Project: интернационализация и локализация