Создавать пользовательский интерфейс во Flutter стало довольно просто благодаря всем виджетам, которые предоставляет фреймворк. Про часть из них я рассказал в своей прошлой статье. Однако приложение должно быть не только красивым, но и функциональным. Порой нам приходится совершать огромное количество действий или перемещать по несколько раз данные между экранами. Во Flutter навигация от одного экрана к другому возможна благодаря Навигатору - простому виджету, который поддерживает стек Маршрутов, или, проще говоря, историю посещенных экранов/страниц.
В интернете вы сможете найти множество статей, рассказывающих о том, как перейти на новый экран или выйти с текущего, но эта статья - нечто большее. В первую очередь она будет посвящена большинству методов работы Навигатора и описанию конкретных примеров.
Предисловие
Вы где-то упоминали о маршрутах, что это такое?
Маршруты - это абстракция для экрана или страницы приложения. Например, '/home' выведет вас на главный экран (HomeScreen) или '/login' выведет вас на экран авторизации (LoginScreen). '/' будет вашим начальным маршрутом. Это может показаться очень похожим на маршрутизацию в разработке REST API. Таким образом, '/' может действовать как рут (root).
Вот как можно определить маршруты в своем приложении Flutter:
new MaterialApp(
home: new Screen1(),
routes: <String, WidgetBuilder> {
'/screen1': (BuildContext context) => new Screen1(),
'/screen2' : (BuildContext context) => new Screen2(),
'/screen3' : (BuildContext context) => new Screen3(),
'/screen4' : (BuildContext context) => new Screen4()
},
)
Screen1(), Screen2() и т.д. - это имена классов для каждого экрана.
. . .
Push, push, push.
Если вы хоть немного разбираетесь в структуре данных, то знаете о стеках. Если у вас есть базовые знания о стеках, то вы знаете о методах "push" и "pop".
Если нет, то push - это добавление элемента на вершину стека элементов, а popping - удаление верхнего элемента из того же стека.
Поэтому в случае с Flutter, когда мы переходим на другой экран, мы используем методы push, а виджет Navigator добавляет новый экран на вершину стека. Естественно, методы pop удаляют этот экран из стека.
Итак, давайте перейдем к кодовой базе нашего тестового проекта и посмотрим, как мы можем перейти с Экрана 1 на Экран 2. Вы можете поэкспериментировать с методами, запустив пробное приложение.
new RaisedButton(
onPressed:(){
Navigator.of(context).pushNamed('/screen2');
},
child: new Text("Push to Screen 2"),
),
Это было вкратце.
Это было действительно так. С помощью методов pushNamed мы можем перейти к любому экрану, маршрут которого определен в файле main.dart. Для удобства мы называем их namedRoute. Применение этого метода довольно простое. Простое перемещение.
Метод "pop"
Теперь, когда мы хотим избавиться от последнего посещенного экрана, которым в данном случае является Экран 2, нам нужно будет удалить Маршруты из стека Навигатора с помощью методов pop.
Navigator.of(context).pop();
Помните, что эта строка кода находится внутри вашего метода onPressed.
При использовании Scaffold обычно нет необходимости открывать маршрут, поскольку Scaffold автоматически добавляет кнопку "back" (назад) в AppBar, которая при нажатии вызывает Navigator.pop(). Даже в Android нажатие кнопки "back" на устройстве сделает то же самое. Тем не менее, этот метод может понадобиться в других случаях, например, для вызова AlertDialog, когда пользователь нажимает на кнопку Cancel.
Почему вместо возврата на предыдущий экран следует использовать метод pop?
Представьте, что у вас есть приложение для бронирования отелей, в котором гостиницы перечислены в нужном вам месте. Нажав на любой элемент списка, вы перейдете на экран с более подробной информацией об отеле. Вы выбираете один из них, но отель вам не нравится, и вы хотите вернуться к списку. Если вы вернетесь к экрану списка отелей (HotelListScreen), вы также сохраните экран с подробной информацией (DetailsScreen) в стеке. Таким образом, нажатие кнопки "back" вернет вас к экрану DetailsScreen. Так запутано!
Попробуйте сделать это на практике. Запустите пример приложения, обратите внимание на панель приложений на Экране 1, у нее нет кнопки возврата, потому что это начальный Route (или домашний экран). Теперь нажмите Push to Screen 2 и вместо кнопки назад, нажмите Push to Screen 1 вместо Pop и обратите внимание на панель приложений на Экране 1. Нажатие на вновь появившуюся клавишу "back" вернет вас на Экран 2, а вам это не нужно.
maybePop
А что, если вы находитесь на начальном Маршруте, и кто-то по ошибке попытался открыть этот экран. Появление единственного экрана в стеке приведет к закрытию вашего приложения, потому что тогда у него не будет маршрута для отображения. Вы определенно не хотите, чтобы у вашего пользователя был такой неприятный опыт. Вот тут-то и вступает в дело maybePop(). Это значит, что вы можете открыть приложение, только если это возможно. Попробуйте, нажмите на кнопку maybePop на Экране 1, и это ничего не даст. Потому что там нечего нажимать. Теперь попробуйте сделать то же самое с Экраном 3, и он применит метод pop к экрану. Потому что он может.
canPop
Прекрасно, что мы можем это сделать , но как я могу узнать, является ли это начальным маршрутом? Было бы неплохо, если бы я мог выводить пользователю какое-нибудь предупреждение в таких случаях.
Отличный вопрос, просто вызовите метод canPop(), и он вернет значение true, если этот маршрут может быть открыт, и false, если это невозможно.
Попробуйте оба этих метода canPop и maybePop на Экране 1 и Экране 3 и вы увидите разницу. Выведенные значения для canPop будут отображаться на вкладке консоли вашей IDE.
. . .
Метод push немного сильнее
Давайте вернемся к более сильным push-методам и углубимся в них. Теперь мы поговорим о замене старого маршрута на новый. У нас есть два метода, которые могут это сделать - pushReplacementNamed и popAndPushNamed.
Navigator.of(context).pushReplacementNamed('/screen4');
//and
Navigator.popAndPushNamed(context, '/screen4');
Попробуйте поэкспериментировать с обоими вариантами на Экране 3 в тестовом приложении. Обратите внимание на анимацию выхода и входа в каждом случае. pushReplacementNamed выполнит анимацию входа, а popAndPushNamed выполнит анимацию выхода. Мы можем использовать это для следующих возможных случаев.
Пример использования: pushReplacementNamed
Если пользователь успешно вошел в систему и теперь находится на экране приборной панели (DashboardScreen), то вы ни в коем случае не хотите, чтобы пользователь вернулся на экран авторизации (LoginScreen). Поэтому маршрут входа должен быть полностью заменен маршрутом приборной панели. Другим примером может быть переход на главный экран (HomeScreen) с экрана заставки (SplashScreen). Он должен отображаться только один раз, и пользователь не должен вернуться к нему из HomeScreen снова. В таких случаях, поскольку мы переходим на совершенно новый экран, мы можем использовать этот метод для его свойства enter animation.
Пример использования: popAndPushNamed
Предположим, вы создаете приложение для шоппинга, которое отображает список товаров на экране списка товаров (ProductsListScreen), а пользователь может применять фильтры на экране фильтров (FiltersScreen). Когда пользователь нажимает на кнопку Apply Changes, FiltersScreen должен применить методы push и pop к ProductsListScreen с новыми значениями фильтров. Здесь более уместным будет свойство анимации выхода popAndPushNamed.
. . .
Почти заканчиваем...
Мы почти подошли к концу статьи. Ну, почти. В этом разделе мы рассмотрим следующие три метода: pushNamedAndRemoveUntil и popUntil.
Пример использования: pushNamedAndRemoveUntil
Итак, в основном вы создаете приложение, похожее на Facebook/Instagram, где пользователь входит в систему, прокручивает свою ленту, просматривает различные профили и, закончив, хочет выйти из приложения. После выхода из приложения вы не можете просто применить метод push к главному экрану (HomeScreen) (или к любому другому экрану, который должен отображаться после выхода из приложения) в таких случаях. Вы хотите удалить все маршруты в стеке, чтобы пользователь не мог вернуться к предыдущим маршрутам после выхода из приложения.
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', (Route<dynamic> route) => false);
Здесь (Route<dynamic> route) => false обеспечит удаление всех маршрутов, предшествующих маршруту, к которому был применен метод push.
Теперь вместо того, чтобы удалять все маршруты перед маршрутами, к которыми был применен метод push, мы можем удалить только определенное количество маршрутов. Возьмем в качестве примера приложение Shopping или, по сути, любое приложение, которое требует платежных операций.
Поэтому в этих приложениях, как только пользователь завершил платежную операцию, все экраны, связанные с транзакцией или корзиной, должны быть удалены из стека, а пользователь должен быть переведен на экран подтверждения платежа (PaymentConfirmationScreen). Нажатие на кнопку "назад" должно возвращать пользователя только на экран списка товаров (ProductsListScreen) или на главный экран (HomeScreen).
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));
Итак, в соответствии с фрагментом кода, мы применяем метод push к Экрану 4 и удаляем все маршруты до экрана Экрана 1. Таким образом, наш стек будет выглядеть следующим образом.
Пример использования: popUntil
Представьте, что вы создаете приложение похожее на Google Forms или приложение, позволяющее заполнять и организовывать формы Google. Теперь какой-то пользователь должен заполнить длинную форму из трех частей, которая может отображаться на трех последовательных экранах в мобильном приложении. На третьей части формы пользователь решает отменить заполнение формы и нажимает на Cancel. Все предыдущие экраны, связанные с формой, должны вылететь. Пользователь в этом случае должен вернуться на HomeScreen или DashboardScreen, потеряв все данные, связанные с формой (что мы и хотим в таких случаях). Здесь мы не будем предлагать ничего нового, просто вернемся к предыдущему маршруту.
Navigator.popUntil(context, ModalRoute.withName('/screen2'));
. . .
Где находятся данные?
В большинстве приведенных выше примеров я просто использую метод push для нового маршрута, не отправляя никаких данных, но такой сценарий крайне маловероятен в реальном приложении. Чтобы отправить данные, мы должны использовать Navigator для применения метода push к новому маршруту MaterialPageRoute с нашими данными (здесь это имя пользователя (userName)).
String userName = "John Doe";
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) =>
new Screen5(userName)));
Чтобы получить значения в Экране 5, мы просто добавим параметризованный конструктор:
class Screen5 extends StatelessWidget {
final String userName;
Screen5(this.userName);
@override
Widget build(BuildContext context) {
print(userName)
...
}
}
Это означает, что вы можете использовать MaterialPageRoute не только для метода push, но и для pushReplacement, pushAndPopUntil и т. д. По сути, вы можете убрать ключевое слово из описанных выше методов, и первый параметр теперь будет принимать MaterialPageRoute вместо строки namedRoute.
Верни мне данные, парень
Вы также можете захотеть вернуть данные с нового экрана. Например, если вы создаете приложение "Будильник", и чтобы установить новый сигнал для будильника, вы должны вывести диалог со списком вариантов звукового сигнала. Очевидно, что вам понадобятся данные о выбранном элементе после того, как диалоговое окно будет открыто. Этого можно добиться следующим образом:
new RaisedButton(onPressed: ()async{
String value = await Navigator.push(context, new MaterialPageRoute<String>(
builder: (BuildContext context) {
return new Center(
child: new GestureDetector(
child: new Text('OK'),
onTap: () { Navigator.pop(context, "Audio1"); }
),
);
}
)
);
print(value);
},
child: new Text("Return"),)
Попробуйте это на Экране 4 и проверьте в консоли значения печати.
Также обратите внимание: когда маршрут используется для возврата значения, параметр типа "маршрут" должен соответствовать типу результата метода pop. Здесь нам нужно было вернуть строку, поэтому мы использовали MaterialPageRoute<String>. Однако ничего не страшного, можно также не указывать тип.
Воу, информации было очень много
Это действительно так, и я мог бы просто описать методы и их реализацию, но поскольку методов Navigator очень много, я хотел объяснить их с помощью примеров, взятых из реальных приложений. Надеюсь, это помогло вам расширить свой кругозор в отношении Navigator.
Переведено на русский язык с сайта: https://medium.com/flutter-community/flutter-push-pop-push-1bb718b13c31