Android上でのFlutterアプリライフサイクル

# 最初に
Androidアプリではライフサイクルにとても気を遣う必要があります。なので、AndroidエンジニアとしてはFlutter上ではどのようなライフサイクル管理が必要か気になったので調べてみました。

環境はFlutter v1.5.4、Android 9.0です。

# Flutterの画面ライフサイクル
Androidではうんざりするほどの数のライフサイクルコールバックがありますが、Flutterのライフサイクルはかなりシンプルです。ドキュメントの[AppLifecycleState](https://api.flutter.dev/flutter/dart-ui/AppLifecycleState-class.html)に状態があります。
要約すると次のように書かれています。

- resumed  
アプリが表示され、ユーザの入力を受け付けます。
-  inactive  
アプリは非アクティブで、ユーザの入力を受け付けていない状態です。
Androidでは、アプリはフォアグラウンドで表示されていますがフォーカスを失っていてユーザの入力を受け付けない状態です。
- paused  
アプリは一時停止状態で、ユーザからは見えず、入力も受け付けていません。
Androidアプリではsuspending状態にいつでも入る可能性があることを想定してください。
- suspending  
アプリは一時的に中断されます。iOSでは現在未使用です。

なんとなくAndroidのライフサイクルとの対応が見えてきますね。

# Flutterのライフサイクル取得方法

Flutterで、ライフサイクルを取得するにはWidgetsBindingObserverを実装し、didChangeAppLifecycleStateメソッドをオーバーライドします。具体的には次のようにします。

```dart
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    debugPrint("lifecycle state change=$state");
  }
```

# 実際に動かしてみる
- アプリ起動時  
inactive -> resumedが連続で発生します。inactiveが先に来ているのが奇妙ですね・・・。
- HOMEボタンを押してアプリを非表示にする  
inactive -> pausedが連続で発生します。ここでもinactiveが発生します。
- HOMEボタンを押してバックグラウンドに移動したアプリを履歴から再表示する  
inactive -> resumedが連続で発生します。やっぱりinactiveが先に来ます。
- 画面回転させる  
何もイベントは発生しません。

他にもいろいろやってみますが、suspendingは発生しませんでした。また、アプリ表示時にinactiveが来てresumedが来るのが奇妙です・・・。

# Flutterエンジンのソースコードで確認してみる
アプリを非表示にしても表示にしてもinactiveが発生するのが奇妙なので、どんなソースコードになっているのか確認してみましょう。
FlutterView.javaでAndroidのライフサイクルイベントをFlutterのライフサイクルイベントに変換しています。

```java
    public void onStart() {
        lifecycleChannel.appIsInactive();
    }

    public void onPause() {
        lifecycleChannel.appIsInactive();
    }

    public void onPostResume() {
        for (ActivityLifecycleListener listener : mActivityLifecycleListeners) {
            listener.onPostResume();
        }
        lifecycleChannel.appIsResumed();
    }

    public void onStop() {
        lifecycleChannel.appIsPaused();
    }
```

おっと、なぜかonStartでinactiveイベントを送信しています。onStartとonPauseが同じinactiveイベントです。resumedがAndroidのonResume、pausedがAndroidのonStopに対応している確認が取れましたね。しかしsuspendingはどこでも送信していません。Flutterエンジンのソースコードをgrepしても、定義は見つかりますが送信している場所はありませんでした。iOSでは使われていないとありましたが、Androidでも使われていないのでしょうか。Gitのログから確認してみましょう。

# Suspendingが使われていない理由
Gitのログを漁ると、このPR [Do not pause rendering when android activity loses focus](https://github.com/flutter/engine/pull/4848/files)が見つかります。

元々は、pausedがAndroidのonPause、suspendingがAndroidのonStopに対応していたことがわかります。そして、バグの修正のためonStopにはpausedが対応することになったようです。Androidにマルチウインドウ機能が追加され、onPauseとonStopの意味合いが若干変わったため仕方ない修正ですね。
この時点でsuspendingイベントは発生しなくなりました。

# アプリ表示時にinactiveイベントが発生する理由
次に、なぜAndroidのonStartでinactiveイベントを送信しているか見てみます。

[add onStart hook which places flutter in an inactive state](https://github.com/flutter/engine/pull/5177/files)のPR
で[Android's FlutterView works unexpected when there's a translucent activity above it.](https://github.com/flutter/flutter/issues/16856)のバグを修正したようです。
onStartが呼ばれて、onResumeが呼ばれないケースで期待通りの動作をして欲しいのでonStartでinactiveイベントを送信しているようです。
inactiveは、アプリは表示されていますがフォーカスを得ていない状態なので、確かにonStartで送信されるべきイベントです。

# まとめ
AndroidのライフサイクルコールバックとFlutterのライフサイクルイベントの対応は次の通りです。

- inactive  
onStart、onPause
- resumed  
onResume
- paused  
onStop
- suspending  
Androidでも未使用

Flutterのライフサイクルと、Androidのライフサイクルの対応がわかりました。ただ、バグ修正のために結構思い切った変更が入るので、なんとなくWidgetsBindingObserverも用いたFlutterのライフサイクルの取得は、アプリ開発者が気にするための物では無く、Flutter自体が正しく動作するために利用しているものという印象を受けました。