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 в своем приложении и:

  1. Передайте его поддереву через context по provider (пропустите этот шаг, если вы предпочитаете использовать другие методы, например get_it)
  2. Установите его как наблюдателя за 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(). Возможности просто безграничны.