[Flutter] NavigatorObserver/RouteObserverを利用した画面遷移の検出

# 1. はじめに

複数画面を遷移するアプリの場合、画面遷移時のアクション (画面が最初に表示された時、別の画面を表示するためにその画面から離れるなど) を定義したい時があります。本記事は、この場合の対応方法をまとめた記事です。

# 2. そもそもFlutterで画面管理を行っているのは？
Flutterでは、[Navigator](https://api.flutter.dev/flutter/widgets/Navigator-class.html)が画面遷移 (Pop, Push) をスタックのようなイメージで管理しています。そして、`Route`と呼ばれるWidgetを画面に表示しており、NavigatorがこのRouteを切り替えることによって画面遷移しています。

Route関連のソースコードは[ここ](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/routes.dart)にありますので、興味があれば覗いてみてください。

# 3. 画面遷移の検出方法

やり方は2通りあります。

一つは、これから説明する`NatigatorObserver`を継承した画面遷移検出用のWidgetを自分用に作成する方法です。もう一つは、各画面 (ただしStatefulWidget) ごとに検出するための`RouteObserver`を利用する方法です。

前者は少し面倒なので、StatefulWidgetの場合であれば後者を使ったほうが楽です。

## [方法1] NatigatorObserverを利用した画面遷移の検出

Navigatorは、引数として[NavigatorObserver](https://api.flutter.dev/flutter/widgets/NavigatorObserver-class.html)というObserverのリスト (List<NavigatorObserver> observers: const []) を指定することができます。このNavigatorObserverを利用することで、各画面の遷移を検出することが可能です。

```Dart:MaterialAppでの対応
Widget build(BuildContext context) {
  ...
  return MaterialApp(
    title: 'sample',
    home: Page1(),
    routes: {
      '/page1' : (_) => Page1(),
      '/page2' : (_) => Page2(),
      '/page3' : (_) => Page3(),
    },
    navigatorObservers: [
      MyNavigatorObserver(), // <= ここ!
    ],
  );
}
```

以下のように、NavigatorObserverクラスを継承した自前クラスを定義することで、画面変化イベントを検出出来ます。

```Dart:MyNavigatorObserverクラス
import 'package:flutter/material.dart';

class MyNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route route, Route previousRoute) {
    super.didPush(route, previousRoute);

    var previous = '';
    if (previousRoute == null) {
      previous = 'null';
    }else {
      previous = previousRoute.settings.name;
    }
    print('Current:' + route.settings.name + '  Previous:' + previous);
  }

  @override
  void didPop(Route route, Route previousRoute) {
    super.didPop(route, previousRoute);

    var previous = '';
    if (previousRoute == null) {
      previous = 'null';
    }else {
      previous = previousRoute.settings.name;
    }
    print('Current:' + route.settings.name + '  Previous:' + previous);
  }
}
```

## [方法2] RouteObserverを利用した画面遷移の検出

[RouteObserver](https://api.flutter.dev/flutter/widgets/RouteObserver-class.html)を利用した方法を紹介します。RouteObserverはRouteのStateの変化を検出し、通知してくれる、NavigatorObserverを継承したクラスです。

まず、RouteObserverのインスタンスをNavigatorObserverのリストに渡します。

```Dart:MaterialAppでの対応
final RouteObserver<PageRoute> _routeObserver = RouteObserver<PageRoute>(); // <= ここ！

Widget build(BuildContext context) {
  ...
  return MaterialApp(
    title: 'sample',
    home: Page1(),
    routes: {
      '/page1' : (_) => Page1(_routeObserver), // <= ここ! (画面遷移を検出したい対象のWidget)
      '/page2' : (_) => Page2(),
      '/page3' : (_) => Page3(),
    },
    navigatorObservers: [
      _routeObserver, // <= ここ!
    ],
  );
}
```

次に、[RouteAware](https://api.flutter.dev/flutter/widgets/RouteAware-class.html)クラスを継承しつつ、以下のサンプルコードのように対応していきます。

```Dart:
class Page1 extends StatefulWidget {
  final RouteObserver<PageRoute> routeObserver;
  const Page1(this.routeObserver);

  @override
  _Page1State createState() => _Page1State();
}

class _Page1State extends State<Page1> with RouteAware {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    widget.routeObserver.subscribe(this, ModalRoute.of(context));
  }

  @override
  void dispose() {
    widget.routeObserver.unsubscribe(this);
    super.dispose();
  }

  @override
  void didPush() {
    // 画面が初めて表示 (Push) される時にコールされます。
  }

  @override
  void didPop() {
    // この画面から別の画面に遷移する (Pop) 場合にコールされます。
  }

  @override
  void didPushNext() {
    // この画面から別の画面をPushする場合にコールされます (この画面はPopされずにそのまま残る場合)。
  }

  @override
  void didPopNext() {
    // 一度、別の画面に遷移したあとで、再度この画面に戻ってきた時にコールされます。
  }
...
}
```