Введение

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

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

Для реализации и применения навигации во Flutter можно использовать либо виджет Navigator, не требующий установки какого-либо пакета, либо установить такой пакет, как AutoRoute.

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

Почему AutoRoute?

AutoRoute основан на Navigator 2.0, декларативном API, который устанавливает стек истории Navigator и включает виджет Router для настройки Navigator на основе состояния приложения и системных событий.

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

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


Установка

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

dependencies:                    
auto_route: ^7.1.0

dev_dependencies:
auto_route_generator: ^7.0.0
build_runner: ^2.4.4

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

flutter packages get

Настройка

Для автоматической генерации навигации сначала создадим файл router.dart и поместим его в каталог lib, где будет находиться весь код.

В нем создадим класс Router и аннотируем его @AutoRouterConfig. Затем расширим $YourClassName и переопределим геттер маршрутов, содержащий список объектов AutoRoute. Таким образом упаковываем все наши маршруты.

@AutoRouterConfig()      
class AppRouter extends $AppRouter {

@override
List<AutoRoute> get routes => [
/// сюда помещаются маршруты
];
}

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

Создание пользовательского интерфейса

Теперь, когда установка зависимостей завершена, создадим три простых экранных виджета в каталоге lib.

Первый экран представляет собой начальный пункт назначения и будет иметь кнопку, которая переводит на второй экран. Этот второй экран снабжен кнопкой “Back” (“Назад”) на AppBar для навигации назад и кнопкой, которая направляет на третий экран. Этот третий экран будет иметь кнопку “Back” и кнопку, которая переводит на первый экран и очищает стек.

Теперь применим все это на практике!

  • Первый экран:
@RoutePage()
class FirstScreen extends StatefulWidget {
const FirstScreen({super.key});

@override
State<StatefulWidget> createState() => _FirstScreenState();
}

class _FirstScreenState extends State<FirstScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("First Screen"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"First Screen",
style: Theme.of(context).textTheme.headlineMedium,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {
/// здесь располагается навигация
},
),
);
}
}
  • Второй экран:
@RoutePage()
class SecondScreen extends StatefulWidget {
const SecondScreen({super.key});

@override
State<StatefulWidget> createState() => _SecondScreenState();
}

class _SecondScreenState extends State<SecondScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Second Screen"),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
/// здесь располагается навигация
},
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Second Screen",
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {
/// здесь располагается навигация
},
),
);
}
}
  • Третий экран:
@RoutePage()
class ThirdScreen extends StatefulWidget {
const ThirdScreen({super.key});

@override
State<StatefulWidget> createState() => _ThirdScreenState();
}

class _ThirdScreenState extends State<ThirdScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Third Screen"),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
/// здесь располагается навигация
},
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Third Screen",
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.restart_alt),
onPressed: () {
/// здесь располагается навигация
},
),
);
}
}

Для создания маршрутов были аннотированы страницы виджетов с помощью @RoutePage(), что позволяет Router их построить.

Как видно из приведенного выше кода, на всех страницах блок onPressed() оставлен пустым для каждого события нажатия. Код для навигации будет добавлен немного позже.

Теперь просто запустим генератор с помощью команды, создающей файл router.gr.dart в директории. Таким образом, осуществим обработку навигации.

flutter packages pub run build_runner watch

Обратите внимание: если понадобится добавить новые экраны в приложение после генерации файла для навигации, нужно просто снова выполнить вышеуказанную команду, чтобы обновить сгенерированный файл. Не забудьте при этом объявить добавленный экран с помощью аннотации @RoutePage().

Завершение настройки

Теперь пришло время добавить сгенерированные маршруты в созданный ранее List.

@override
final List<AutoRoute> routes = [
AutoRoute(page: FirstRoute.page, path: '/'),
AutoRoute(page: SecondRoute.page),
AutoRoute(page: ThirdRoute.page),
];

Здесь символ “/” в path для этого маршрута означает, что страница используется в качестве первого экрана библиотекой пакета AutoRoute.

Имена маршрутов автоматически генерируются пакетом, по умолчанию он заменяет слова “Page” (“Страница”) и “Screen” (“Экран”) на “Route” (“Маршрут”). Например, FirstScreen будет FirstRoute.

Автоматически генерируемые имена маршрутов с суффиксом Route могут быть довольно длинными, например ProductDetailsPage будет ProductDetailsPageRoute.

Можно заменять соотносимые части в именах маршрутов, реализовав замену по схеме whatToReplace,replacement. При этом whatToReplace (то, что надо заменить) и replacement (замена) должны быть разделены запятой, например Page,Route. Поэтому ProductDetailsPage будет ProductDetailsRoute.

Page|Screen,Route реализуется по умолчанию, кроме случаев, когда указано имя маршрута.

Есть два способа задать собственные имена для маршрутных страниц.

  • На самой странице внутри аннотации @RoutePage() можно добавить атрибут name и задать желаемое имя, например @RoutePage(name: “FirstScreen”). Это самый простой способ, но он может оказаться слишком утомительным при установке на каждой странице экрана, поэтому выберем второй.
  • В файле router.dart внутри аннотации @AutoRouterConfig() добавим атрибут replaceInRouteName, и здесь значение по умолчанию будет “Page|Screen,Route”. Это означает, что, если в имени страницы есть либо “Page”, либо “Screen”, оно будет заменено на “Route”. Поэтому, чтобы сохранить имя маршрута, добавим значение “Route,Page|Screen”, например @AutoRouterConfig(replaceInRouteName: “Route,Page|Screen”).

Теперь займемся рефакторингом класса route с выбранными именами страниц. Ниже приведен окончательный код.

@AutoRouterConfig(replaceInRouteName: "Route,Page|Screen")
class AppRouter extends $AppRouter {
@override
final List<AutoRoute> routes = [
AutoRoute(page: FirstScreen.page, path: '/'),
AutoRoute(page: SecondScreen.page),
AutoRoute(page: ThirdScreen.page),
];
}

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

@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _appRouter.config(),
title: "Flutter Navigation",
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
);
}

Навигация в действии

Теперь навигация готова к использованию, поэтому реализуем логику для событий нажатия в пустом блоке onPressed() на каждой странице.

Начнем поток с первого экрана.

floatingActionButton: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {
AutoRouter.of(context).push(
SecondRoute(title: "Second Screen"),
);
},
),

Как видно из приведенного выше кода, чтобы привести навигацию в действие с помощью созданной FloatingActionButton, мы использовали объект AutoRouter и вызвали метод push(), передав ему на экране конечный пункт назначения, к которому нужно перейти.

Этот метод, как следует из названия, выдвигает пункт назначения поверх текущего, создавая стек в навигации.

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


На втором экране обрабатываем переданный аргумент в конструкторе класса. Затем при необходимости просто сошлемся на экземпляр widget, чтобы установить Text в AppBar.

/// ...

final String title;

const SecondScreen({super.key, required this.title});

/// ...

appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
AutoRouter.of(context).pop();
},
),
),

/// ...

floatingActionButton: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {
AutoRouter.of(context).push(
ThirdRoute(title: "Third Screen"),
);
},
),

Для навигации назад мы использовали кнопку IconButton в AppBar и вызвали метод pop() на объекте AutoRouter. Таким образом, мы очистили стек навигации и вернулись на предыдущий экран.

Плавающая кнопка FloatingActionButton переводит нас на следующую страницу.


На третьем экране обрабатываем логику для кнопки “Назад”, как делали раньше. Она переводит нас на вторую страницу.

floatingActionButton: FloatingActionButton(
child: const Icon(Icons.restart_alt),
onPressed: () {
AutoRouter.of(context).popUntilRoot();
},
),

Для кнопки FloatingActionButton, как можно видеть выше, мы использовали popUntilRoot(). Это очищает весь стек навигации, что переводит нас на первую страницу, поскольку стек очищен. Это был самый первый пункт назначения, который мы определили в списке маршрутов.


Итоги

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

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Andrea Briasco: Navigation in Flutter with AutoRoute

Предыдущая статьяТехника каррирования в JavaScript: суть, преимущества, примеры
Следующая статьяScyllaDB в K8S: как справляться с интенсивными рабочими нагрузками на спотовых экземплярах без простоев