AndroidアプリのプロダクトコードにFlutterのAdd-to-appを試しに導入検証してみた

はじめに

こちらはアソビュー! - Qiita Advent Calendar 2024 - Qiitaの14日目(A面)の記事です。

アソビューのAndroidアプリ開発を担当している田澤です。 最近はFlutterKaigi2024に行ってきました。Flutterへのリプレイスに関するセッションがいくつかあり、Add-to-appを利用してるようでした。今回はプロダクトコードのAndroidアプリにAdd-to-appを試しに導入してみた話となります。なぜプロダクトコードで行うかというとサンプルコードでは各種依存関係による修正作業が発生の有無がわからない、想定外の導入障壁が発生するかもしれないという考えからとなっています。なので導入というよりは検証に近いものとなります。

注)試しになので導入決定という話ではありません

Flutter(Add-to-app)を試す理由

エンジニア観点からネイティブ開発の課題点をいくつか感じておりマルチプラットフォームであればある程度解消できるのではという期待からとなります。

リソースの確保

プラットフォームごと(現状はiOSとAndroid)に開発リソースが必要となります。それだけでなく、品質とスピードを担保してくことも重要です。それらを維持するのは規模が大きくなるにつれて難しくなってきます。メンテンナンス規模も膨らむのでなおのことです。

採用の観点もあります。現状モバイルエンジニアの採用が難しいと感じている方々はいるかと思います。リソースが変わらない状態のままアプリの規模を大きくしつつ継続していくのは困難をむかえるだろうと思われます。

流動性

マルチプラットフォーム化ができると、iOS、Androidの機能実装を行う場合にエンジニアが一人で対応可能になることがあると想定されます。その分、他のメンバーはWebフロント、バックエンドなど別領域の開発に従事できる、別の施策を進められるなど開発要員の流動性が高められることが期待できます。 アソビューでは特定の技術に縛られずマルチファンクショナルな組織を目指しているため文化的に合っていると考えられます。

プラットフォーム間の仕様差異

開発が進むと僅かながらOS間で仕様差が発生していきます。 iOS、Androidそれぞれの実装の難易度、工数によって生じるもの、各OSらしさの振る舞いに合わせた故に生じるもの、細かい箇所まで仕様が詰めきれておらず生じるものなどがあります。

細かい仕様差でも積み重なると徐々に負荷となって表れます。 仕様確認の調査や、QAのコストが肥大していきます。開発だけでなく利用しているユーザーにも影響を与える可能性もあります。

弊社のアソビューアプリで言えば、ユーザーは待ち時間を少なくスムーズにチェックインしたいと思います。プラットフォーム間で仕様差異が存在すると施設側のオペレーションが複雑になる。例えば、『施設側がユーザーにアプリ操作の案内を行っているが、Androidアプリだけ想定外の動きをするため説明がスムーズにできない』といった事態も起こり得ます。つまり、ユーザーにも施設の方々にも価値提供の低下が起こり得ます。

Add-to-appとは

そこで検討したのがFlutterのAdd-to-appです。既存のネイティブコードなプロジェクト(iOS、Androidアプリ)へ部分的にFlutterを導入していくことができます。公式ドキュメントも十分整備されており敷居も低いと感じています。また、方法によっては他開発者、プロダクトコードへの影響も少なく使える点も良いのではないでしょうか。

事前準備

動作環境

  • macOS 15.0.1
  • Android Studio Koala | 2024.1.1
    • Flutter Plugin 82.0.2
    • Dart Plugin 241.18808
 % flutter --version
Flutter 3.24.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 603104015d (6 weeks ago) • 2024-10-24 08:01:25 -0700
Engine • revision db49896cf2
Tools • Dart 3.5.4 • DevTools 2.37.3

私のFlutter経験値はv1.0.0が出る前に0→1でのプロダクト開発を少々嗜んだ程度です。

環境構築

Android StudioにFlutterとDartのpluginを導入します。詳細については公式ドキュメントを参照してください。Android Studio and IntelliJ | Flutter

導入

導入手順は基本的に公式ドキュメントを参照するとわかりやすいです。Integrate Flutter | Flutter

導入手法について

AndroidではAdd-to-appの導入方法が2パターン提供されています。以下の表にまとめました。

名前 方法 メリット デメリット
Android archive FlutterのコードをAARにビルドしてホストアプリが参照する。 他の開発者がFlutterの開発環境を準備する必要がない。 Flutterを使用した開発が増えAARを配布する回数が増えてくると運用が手間になる
Module source code Flutteコードとホストアプリを共通ディレクト内で保持。一度のビルドでFlutter、Androidが実行される。 Android archiveのようにAARにビルドして都度配布する手間がなくなる。FlutterとAndroid双方を同時に開発を進めるのに向いている。 他の開発者がFlutterの開発環境を用意する必要がある

今回はAndroid archiveを試した後、Module source codeでの導入も行いメリット、デメリットについて検討します。

Step 0. Flutter moduleを作成する

はじめにホストアプリに導入するflutter moduleを作成します。 Android Studioの『File>New>New Flutter Project..』を選択します。

New Flutter Project..

New ProjectダイアログでProject typeに Moduleを指定します。 Android archiveではAARを生成して参照するのでProject Locationは管理しやすい箇所に配置して問題ありません。Module source codeではホストアプリと同一階層に配置する必要があります。

New Project ダイアログ

これでflutter moduleの作成が完了です。今回はデフォルトプロジェクトのまま(カウンターアプリ)使用するため、flutter moduleのコードの変更は行いません。

昔から変わらないデフォルトのカウンターアプリ

手法1 Android archive

Step 1-1. flutter moduleのAARを作成

さっそく、Step 0で作成したflutter moduleのAARを生成します。 flutter moduleのディレクトリで以下のコマンドを実行します。

flutter build aar

が、私の環境では以下のエラーが発生しました。

Could not open cp_init generic class cache for initialization script 'flutter/packages/flutter_tools/gradle/aar_init_script.gradle' (.gradle/caches/7.5/scripts/7jzh2brspa6n2p7pnesv902ai).
BUG! exception in phase 'semantic analysis' in source unit 'BuildScript' Unsupported class file major version 65

原因はAGPのバージョンとflutter module生成時にデフォルト指定されているgradle wrapperのgradleのバージョンに差異があるためでした。 今回の環境ではAGPはバージョン8.5であったためgradleのバージョンは8.7が必要でした。 そのため今回はflutter moduleのgradleバージョンを以下のように修正して対応しました。

// .android/gradle/wrapper/gradle-wrapper.properties

- distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
+ distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip

各々のAndroid Studioのバージョンとそれに紐づくAGPバージョンによってはこのようなエラーが発生します。バージョンの対応表は公式ページにまとめられているので適宜確認すると良いでしょう。 Android Gradle plugin and Android Studio compatibility  |  Android Studio  |  Android Developers Update Gradle  |  Android Studio  |  Android Developers

AARのビルドが成功すると以下のような構成で出力されます。 ※公式ドキュメントより抜粋

build/host/outputs/repo
└── com
    └── example
        └── flutter_module
            ├── flutter_release
            │   ├── 1.0
            │   │   ├── flutter_release-1.0.aar
            │   │   ├── flutter_release-1.0.aar.md5
            │   │   ├── flutter_release-1.0.aar.sha1
            │   │   ├── flutter_release-1.0.pom
            │   │   ├── flutter_release-1.0.pom.md5
            │   │   └── flutter_release-1.0.pom.sha1
            │   ├── maven-metadata.xml
            │   ├── maven-metadata.xml.md5
            │   └── maven-metadata.xml.sha1
            ├── flutter_profile
            │   ├── ...
            └── flutter_debug
                └── ...

1-2. AARをプロダクトに取り込む

修正対象はホストとなるプロダクトのapp配下のbuild.gradleとsettings.gradleになります。settings.gradleでは参照先となるflutterとAARのパスを指定します。build.gradleにはビルドタイプと依存関係を追加します。

// settings.gradle

// repositoriesへ追加
dependencyResolutionManagement {
    repositories {
        maven {
            url "https://storage.googleapis.com/download.flutter.io"
        }
         // AARのパス
        maven {
            url "/path/flutter-module-name/build/host/outputs/repo"
        }
}
// build.gradle

android {
  buildTypes {
    profile {
      initWith debug
    }
  }
}

...

debugImplementation 'com.example.add_to_app.add_to_app_example:flutter_debug:1.0'
profileImplementation 'com.example.add_to_app.add_to_app_example:flutter_profile:1.0'
releaseImplementation 'com.example.add_to_app.add_to_app_example:flutter_release:1.0'

修正後にビルドが通ったら設定は完了です。

1-3. プロダクトとflutter moduleを連携させる

今回はデフォルトのFlutterプロジェクトの画面を表示します。 Flutterを表示するFlutterActivityの定義を追加し呼び出せるようにします。

// android.manifest

 <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/Theme.AppSplash"
     android:windowSoftInputMode="adjustResize" />
// jetpack compose

// 既存のボタンでFlutterActivity起動
// 今回は検索ボックスタップ時に起動するように修正しました
val context = LocalContext.current
MyButton(onClick = {
    context.startActivity(
        FlutterActivity.createDefaultIntent(context)
    )
})

これで実装が完了しました。実行するとFlutterのカウントアップ画面に遷移しカウントアップも動作します。

検索ボックスをタップするとFlutterの画面が表示される

次はModule source codeでのAdd-to-appを試していきます。

手法2 Module source code

Step 2-1. flutter moduleを配置する

Module source codeの場合、Android archiveと異なりflutter moduleはホストアプリのサブプロジェクトとして扱うことが前提となります。ディレクトリ構成を意識する必要があり、ホストアプリとflutter moduleは同一ディレクトリ配下に存在する必要があります。 私の場合はAndroid archiveで既にflutter moduleを作成していたのでflutter moduleを新規作成せずにホストアプリと同一階層に移動させるだけで対応可能でした。

Step 2-2. flutter moduleを参照できるようにする

Module source code用に依存関係を記述します。

// settings.gradle.kts

pluginManagement {
    repositories {
       ...
        maven {
            url "https://storage.googleapis.com/download.flutter.io"
        }
    }
}
        
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
    repositories {
       ...
        maven {
            url "https://storage.googleapis.com/download.flutter.io"
        }
    }
}

// Android archiveから移行した場合は/path/flutter-module-name/build/host/outputs/repoの定義を削除する
maven {
    url "https://storage.googleapis.com/download.flutter.io"
}       
    
include ':app'
    
setBinding(new Binding([gradle: this]))
evaluate(new File(
        settingsDir.parentFile,
        'flutter-module-name/.android/include_flutter.groovy'
))
// build.gradle

// Android archiveから移行した場合はdebugImplementationなどを削除する
implementation(project(":flutter"))

修正後にビルドが通ったら設定は完了です。 最新の環境では上記の対応では動作しない可能性があります。その場合は公式のDeprecated imperative apply of Flutter's Gradle plugins | Flutterを確認するとよいでしょう。

2-3. プロダクトとflutter moduleを連携させる

Android archiveと同じ手順となります。以上でAndroidプロジェクトへのAdd-to-appの手順を一通り触ったことになります。

まとめ

実際にプロダクトコードに導入検証してどうだったか🤔

技術的な視点だけあればflutter moduleと既存プロダクトの各種ツールのバージョンを揃える必要があったものの、比較的難易度は低く導入しやすいのではと感じました。Android archiveとModule source codeでは単純なメリット・デメリットの比較というよりプロジェクトの状況やチーム体制、Flutterへの習熟度に合わせて選択するのが良いと思います。導入検証〜依存関係がない単一画面をAdd-to-appで進めていくまでならAndroid archiveで十分対応が可能ではないかと。Flutterへの知見があり、flutter moduleの規模が大きくなったりネイティブコードとの連携が必要であればModule source codeを採用しても良いかと思います。 状況によってAndroid archiveからModule source codeへ移行してもよいでしょう。

Android archiveのデメリットとして上げたAARの配布について。公式ドキュメントではローカルのpath指定でAARを参照していますが意識ぜずとも運用ができたほうが良さそうです。これに関してはCIで自動的にプライベートなエリアにアップロードして参照できるようにすれば特に問題はないかと思います。ただ、その準備をする手間がデメリットかもしれません。

なお、iOSに関しても検証は進めています。 導入方法にはCocoaPods、iOS frameworks、iOS frameworksとCocoaPodsの組み合わせの3パターンありiOS frameworksでの導入を試しています。 Androidと比べると手間がかかる印象です。将来的に記事にするかもしれないですね。

さいごに

アソビューでは一緒に働くメンバーを大募集しています!カジュアル面談もありますので、少しでも興味があればお気軽にご応募いただければと思います!

www.asoview.com

speakerdeck.com