
Flutter: как реализовать виджеты с учетом маршрута
Иногда вы можете захотеть что-то сделать, основываясь на изменении маршрута.
Например, на некоторых страницах вы хотите отобразить полноэкранный видеопроигрыватель или отобразить интерактивную полноэкранную диаграмму. Вы можете вызвать SystemChrome.setPreferredOrientations() для принудительной горизонтальной ориентации при входе на эти экраны, но не забудьте вызвать его снова, чтобы сбросить ориентацию, когда пользователь покидает экран!
Конечно, вы можете сделать это в методах initState() и dispose(). Но мир не так прост, как насчет того, чтобы пользователь просматривал ваше приложение следующим образом:
Home Screen(vertical) > Chart Screen(horizontal) > Settings Screen(vertical)
Продолжайте нажимать в стек навигации, dispose метод не выполняется, пользователь увидит горизонтальный «Экран настроек». Navigator.pushReplacementNamed()? Это еще хуже. Метод initState() более позднего экрана («Экран настроек») выполняется перед методом dispose() более раннего экрана («Экран диаграммы»). Это не сработает!
Возможные (НО НЕПРАВИЛЬНЫЕ!) решения перед прочтением этой статьи:
Решение 1. Связь
Вы можете обсудить с дизайнером UX и менеджером проекта ограничение перехода маршрутов. Например, после входа в «Экран диаграммы» пользователь может использовать только кнопку «Назад», чтобы вернуться к вертикальному экрану. В этом случае вы можете убедиться, что метод dispose() всегда выполняется при выходе из этих двух экранов.
Но дизайнер UX сейчас недоволен. Вы сильно ограничили его / ее творчество. И клиент может пожаловаться на удобство использования приложения.
Решение 2. Осторожность
Вы можете очень внимательно проверить все обратные вызовы кнопок. При необходимости установите ориентацию перед ее вызовом Navigator.of(context).push(), await, затем снова сбросьте ориентацию, когда пользователь вернется.
На «экране диаграммы»:
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
await Navigator.of(context).push('/settings');
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
Нравится?
Боже мой, это такой бардак! После определенных раундов улучшения / рефакторинга эта логика будет нарушена, вы же не хотите проверять их все еще раз за выпуск, не так ли?
Чтобы ваше приложение не стало недоступным для обслуживания, НИКОГДА НЕ ДЕЛАЙТЕ ЭТО!
RouteAware
RouteAware - это интерфейс для виджетов, которые знают свой текущий Маршрут. Мы собираемся использовать его вместе с RouteObserver.
Прежде всего, добавьте в свой pubspec.yaml следующие строки:
after_layout: ^1.1.0 provider: ^5.0.0
Создайте экземпляр RouteObserver в своем приложении и:
- Передайте его поддереву через
contextпоprovider(пропустите этот шаг, если вы предпочитаете использовать другие методы, напримерget_it) - Установите его как наблюдателя за
Navigatorвашего приложения.
class MyApp extends StatelessWidget {
// create an instance of `RouteObserver`
final RouteObserver<PageRoute> _routeObserver = RouteObserver();
@override
Widget build(BuildContext context) {
return Provider.value(
// provide the `RouteObserver` to subtree widgets through `context`
value: _routeObserver,
child: MaterialApp(
title: 'Flutter Demo',
// set route observer for the `Navigator`
navigatorObservers: [_routeObserver],
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
),
);
}
}
Реализуйте абстрактный класс RouteAwareState:
abstract class RouteAwareState<T extends StatefulWidget> extends State<T> with RouteAware, AfterLayoutMixin<T> {
late RouteObserver<PageRoute>? routeObserver;
bool enteredScreen = false;
// use `afterFirstLayout()`, because we should wait for
// the `initState() completed before getting objects from `context`
@override
// add @mustCallSuper annotation to prevent being overridden
@mustCallSuper
void afterFirstLayout(BuildContext context) {
if (mounted) {
// get the instance of `RouteObserver` from `context`
routeObserver = context.read<RouteObserver<PageRoute>>();
// subscribe for the change of route
routeObserver?.subscribe(this, ModalRoute.of(context) as PageRoute);
// execute asynchronously as soon as possible
Timer.run(_enterScreen);
}
}
void _enterScreen() {
onEnterScreen();
enteredScreen = true;
}
void _leaveScreen() {
onLeaveScreen();
enteredScreen = false;
}
@override
@mustCallSuper
void dispose() {
if (enteredScreen) {
_leaveScreen();
}
routeObserver?.unsubscribe(this);
super.dispose();
}
@override
@mustCallSuper
void didPopNext() {
Timer.run(_enterScreen);
}
@override
@mustCallSuper
void didPop() {
_leaveScreen();
}
@override
@mustCallSuper
void didPushNext() {
_leaveScreen();
}
/// this method will always be executed on enter this screen
void onEnterScreen();
/// this method will always be executed on leaving this screen
void onLeaveScreen();
}
Наконец, расширяет состояние виджета с учетом маршрута с RouteAwareState. И реализуем метод onEnterScreen() и onLeaveScreen():
class ChartScreen extends StatefulWidget {
const ChartScreen();
@override
_ChartScreenState createState() => _ChartScreenState();
}
class _ChartScreenState extends RouteAwareState<ChartScreen> {
@override
void onEnterScreen() async {
print('on enter chart screen');
// set orientation to landscape
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
@override
void onLeaveScreen() async {
print('on leave chart screen');
// set orientation back to portrait
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pushNamed('/settings-screen');
},
icon: Icon(Icons.settings),
),
],
),
body: SafeArea(
child: Stack(
children: [
Center(
child: Text('Chart Screen'),
),
],
),
),
);
}
}
Чтобы увидеть полный исходный код, щелкните здесь.
Клонируйте исходный код и запускайте с flutter run. Вы увидите, что независимо от того, как изменится маршрут, методы onEnterScreen() и onLeaveScreen() будут выполняться безупречно. Вы даже можете создать больше экранов и маршрутов для моделирования более сложного сценария.
Заключение
Вот как можно реализовать виджет с учетом маршрута. Другой вариант использования: в приложении для потоковой передачи данных в реальном времени вы можете подписаться / отказаться от подписки на потоки данных внутри метода onEnterScreen()/onLeaveScreen(). Возможности просто безграничны.