「既存Androidアプリ + Flutter画面」の追加開発

# はじめに
既存のAndroidアプリにFlutterで作った画面を追加することができます。  
「Add-to-app」という機能を使うことで、ネイティブアプリ(Android, iOS)にFlutterで開発した画面やロジックを追加することができます。  
Add-to-appはFlutter 1.12以降で正式に安定版に導入されているので、最新のFlutter Stableバージョンでも利用できます。  
今回は実際にAdd-to-appを使用して、Android(kotlin)のネイティブアプリから、Flutterで開発した画面を呼び出してみたいと思います。

https://docs.flutter.dev/add-to-app

サンプルアプリ

https://github.com/w2406/native_android_app_with_flutter

```
native_android_app_with_flutter  
├── android_app // androidネイティブアプリ
└── flutter_module // Flutterで開発した画面があるモジュール

```
# Androidアプリ作成 【Androidアプリ側】
画面遷移するためのボタンがあるアプリを作成します。  
※今回はJetpack Compose使用

![スクリーンショット 2024-12-22 11.19.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868196/de85bb00-47b1-2d8e-ac1a-484cf0f5b81a.png)

# Flutterのモジュール作成 【Flutterモジュール側】
## Flutterのバージョンを最新に上げる(必要に応じて)
```
flutter upgrade
```

## Flutterのモジュール作成
以下のコマンドを実行して`flutter_module`を作成します。  
このモジュール内にFlutterで開発した画面やロジックを配置します。
```
flutter create -t module flutter_module
```

## AndroidアプリでFlutterモジュールを使えるようにする
Flutterモジュールを汎用的なローカル Maven リポジトリとして使用するためにパッケージ化します。
汎用ローカル Maven リポジトリは、ビルド済みの Android ライブラリ（AARファイル）と、ライブラリの依存関係や設定を記述したPOMファイルで構成されています。

`/flutter_module`で以下のコマンドを実行します。
```
flutter build aar
```

### ！エラー発生
以下のエラーが発生、gradleがサポートしていないjavaを使用していた模様。  
```
FAILURE: Build failed with an exception.

* What went wrong:
※省略
> BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 65
```

```
java --version

openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment Homebrew (build 21.0.5)
OpenJDK 64-Bit Server VM Homebrew (build 21.0.5, mixed mode, sharing)
```

`flutter_module/.android/gradle/wrapper/gradle-wrapper.properties`で以下のようにgradleのバージョン変更
```
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
↓
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
```

ビルド成功

```
flutter_module % flutter build aar

Running Gradle task 'assembleAarDebug'...                         109.4s
✓ Built build/host/outputs/repo
Running Gradle task 'assembleAarProfile'...                        31.3s
✓ Built build/host/outputs/repo
Running Gradle task 'assembleAarRelease'...                        29.9s
✓ Built build/host/outputs/repo

Consuming the Module
  1. Open <host>/app/build.gradle
  2. Ensure you have the repositories configured, otherwise add them:

      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"
      repositories {
        maven {
            url '/../w2406/native_android_app_with_flutter/flutter_module/build/host/outputs/repo'
        }
        maven {
            url "$storageUrl/download.flutter.io"
        }
      }

  3. Make the host app depend on the Flutter module:

    dependencies {
      debugImplementation 'com.example.flutter_module:flutter_debug:1.0'
      profileImplementation 'com.example.flutter_module:flutter_profile:1.0'
      releaseImplementation 'com.example.flutter_module:flutter_release:1.0'
    }


  4. Add the `profile` build type:

    android {
      buildTypes {
        profile {
          initWith debug
        }
      }
    }

To learn more, visit https://flutter.dev/to/integrate-android-archive
```

# Flutterモジュールの依存関係を追加 【Androidアプリ側】
以下の依存関係を追加します。
- `maven("https://storage.googleapis.com/download.flutter.io")`
Dart/Flutterランタイムや関連する依存関係を取得するために必要です。
- `maven(url = "../../flutter_module/build/host/outputs/repo")`
Flutterモジュールを依存関係に追加して、Androidアプリ内から使用できるようにします。
```
settings.gradle

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven("https://storage.googleapis.com/download.flutter.io") ←追加
        maven(url = "../../flutter_module/build/host/outputs/repo") ←追加
    }
}
```

# Flutter画面のActivityを定義【Androidアプリ側】
AndroidアプリにFlutter画面を表示するために、画面(Activity)を`AndroidManifest.xml`に定義します。
```
        <activity
            android:name="io.flutter.embedding.android.FlutterActivity"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:theme="@style/LaunchTheme"
            android:windowSoftInputMode="adjustResize" />
```

`@style/LaunchTheme`のFlutterActivityの起動時のテーマを作成しておきます。
```
res/values/style.xml

<resources>

    <style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@android:color/white</item>
    </style>
</resources>
```

# AndroidアプリからFlutter画面を呼び出す【Androidアプリ側】
画面内で`context.startActivity`を実行することで、Flutter画面をActivityとして呼び出すことができます。

```
@Composable
fun PageOnFlutterButton() {
    val context = LocalContext.current
    Button(onClick = {
        // 遷移処理
        context.startActivity(
            FlutterActivity
                .withNewEngine()
                .initialRoute("/")
                .build(this)
        )
    }) {
        Text("Flutterで作成された画面")
    }
}
```

![画面収録 2024-12-22 12.37.20.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868196/7796a307-708a-2fde-04dd-732682589560.gif)


# ※(任意)アプリ内で1つのFlutterEngineを使用する
上記のようにFlutterEngineを呼び出すと、毎回FlutterEngineを作成されます。  
これにより、リソース不足が発生する原因になったり、Flutter画面を表示するときに初回表示が遅くなります。  
これらを解消するために、1つの`FlutterEngine`をキャッシュ内に保持しておいて、使い回すようにします。

```
// FlutterEngineのキャッシュキー
const val flutterEngineId = "flutter_engine_id"

class MainActivity : ComponentActivity() {
    private lateinit var flutterEngine: FlutterEngine

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

        // 毎回FlutterEngineが作られることで、リソースが無駄・起動が遅くになるのでFlutterEngineをキャッシュに保持
        // Flutterエンジンを事前にインスタンス化
        flutterEngine = FlutterEngine(this)

        // FlutterEngineに初期ルートを設定
        flutterEngine.navigationChannel.setInitialRoute("/");

        // FlutterEngineの初期化
        flutterEngine.dartExecutor.executeDartEntrypoint(
            DartExecutor.DartEntrypoint.createDefault()
        )

        // FlutterEngineを保持して使いまわせるようにする
        FlutterEngineCache
            .getInstance()
            .put(flutterEngineId, flutterEngine)
            
        setContent {...}
省略..
}

@Composable
fun PageOnFlutterButton() {
    val context = LocalContext.current
    Button(onClick = {
        // 遷移処理
        context.startActivity(
            FlutterActivity
                .withCachedEngine(flutterEngineId)
                .build(context)
        )
    }) {
        Text("Flutterで作成された画面")
    }
}

```



# まとめ
Add-to-appを使用して、Android(kotlin)のネイティブアプリから、Flutterで開発した画面を呼び出すことができました。実際に試してみた感想としては、結構簡単にFlutterで開発された画面を呼び出すことができました。
「ネイティブアプリをFlutterで一部分リプレース→すべてリプレース」や「ネイティブアプリの新機能・画面をFlutter開発」に活用できると思いました。