AndroidアプリにFlutterをadd-to-appして複数の画面を扱う

## はじめに
この記事はFlutter公式（ https://flutter.dev/docs/development/add-to-app/multiple-flutters ）に載っている**Multiple Flutter screens or views**を試してみた記事になります。
今までadd-to-appは

* 最初の画面を起動するのに時間がかかる（のでFlutterEngineのキャッシュが必要だった）
* 1画面だけ扱うことを想定している（複数の画面を扱うのは無理では無いがトリッキーな方法が必要だった）
* メモリを食う

などの課題がありました。
特に1画面しか扱うことを想定していないという制限は大きく、プロダクションでは実質使えない状態でした。
Flutter2.0のリリースに伴い実装されたFlutterEngineGroupを使うことでこれらを解決できます！

## 前提・環境
* Android Studio 4.2.1 （**Android Studio CanaryはFlutterをサポートしていないので注意！**）
* Flutter 2.2.1
* Flutter doctorを通していること
* 公式（ https://flutter.dev/docs/development/add-to-app/android/project-setup ）に従い、add-to-appを済ませていること

## 実装
### FlutterEngineGroupの宣言
Applicationクラスを拡張してFlutterEngineGroupを宣言します

```kotlin:App.kt
class App : Application() {
    lateinit var engines: FlutterEngineGroup

    override fun onCreate() {
        super.onCreate()
        engines = FlutterEngineGroup(this)
    }
}
```

### 遷移処理の準備
EngineBindingsクラスを用意します

```kotlin:EngineBindings.kt
package com.example.addtoapptest

import android.app.Activity
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel

interface EngineBindingsDelegate {
    fun onNext()
}

class EngineBindings(activity: Activity, delegate: EngineBindingsDelegate, entrypoint: String) {
    val channel: MethodChannel
    val engine: FlutterEngine
    val delegate: EngineBindingsDelegate

    init {
        val app = activity.applicationContext as App
        // This has to be lazy to avoid creation before the FlutterEngineGroup.
        val dartEntrypoint =
            DartExecutor.DartEntrypoint(
                FlutterInjector.instance().flutterLoader().findAppBundlePath(), entrypoint
            )
        engine = app.engines.createAndRunEngine(activity, dartEntrypoint)
        this.delegate = delegate
        channel = MethodChannel(engine.dartExecutor.binaryMessenger, "multiple-flutters")
    }

    fun attach() {
        channel.setMethodCallHandler { call, result ->
        }
    }

    fun detach() {
        // TODO: Uncomment after https://github.com/flutter/engine/pull/24644 is on stable.
        // engine.destroy();
        channel.setMethodCallHandler(null)
    }
}
```

### Flutter用のActivityの用意
FlutterActivityと上記で宣言したEngineBindingsを継承したActivityを作成します。
<code>entrypoint = ""</code>が呼び出す画面の関数名になりますので、ここはdart側に合わせて適宜変更してください。

```kotlin:SingleFlutterActivity.kt
package com.example.addtoapptest

import android.content.Context
import android.content.Intent
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

class SingleFlutterActivity : FlutterActivity(), EngineBindingsDelegate {
    private val engineBindings: EngineBindings by lazy {
        EngineBindings(activity = this, delegate = this, entrypoint = "firstScreen")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        engineBindings.attach()
    }

    override fun onDestroy() {
        super.onDestroy()
        engineBindings.detach()
    }

    override fun provideFlutterEngine(context: Context): FlutterEngine? {
        return engineBindings.engine
    }

    override fun onNext() {
        val flutterIntent = Intent(this, MainActivity::class.java)
        startActivity(flutterIntent)
    }
}
```

### dart側の準備
main.dartに<code>@pragma('vm:entry-point')</code>で新しいエントリポイントを記述します。
ここで宣言した関数名とkotlin側で呼び出すエントリポイントを一致させましょう。

```dart:main.dart
import 'package:flutter/material.dart';

// MyAppの内容は省略しています
@pragma('vm:entry-point')
void firstScreen() => runApp(MyApp());

void main() => runApp(MyApp());
```

以上です。AndroidManifestにActivityに登録するのを忘れずに！

## 補足

* FlutterEngineGroupはまだまだ実験的な機能とアナウンスされているので、実装はすぐ変わるかもしれません。
* 実験的な機能であるためか、FlutterEngineGroupクラスではプロダクションで使わないでくださいというコメントがあります。導入の際は自己責任で！（ https://api.flutter.dev/javadoc/io/flutter/embedding/engine/FlutterEngineGroup.html ）
* <code>@pragma('vm:entry_point')</code>でも特にエラーが出ない上、なぜかデバッグビルドだと動いてリリースビルドだと動かないという不思議な動作をしました。アンダーバーではなくハイフンなのでご注意を！