はじめに
Android用のTwitterクライアントやmixiビューアといった個人開発アプリ1をいくつか開発しています。
Twitterアプリの TwitPane は 無料版、有料版、Kindle版 という3種類を同一ソースから生成しています。
2013年頃から開発しているので Eclipse+ADT
時代にどうやっていたのかはもう既に覚えていませんが、Android Studio
に移行して Product Flavor
(以下 Flavor
) で気軽に作り分けできるようになったときはずいぶん感動した記憶があります。
AndroidStudioに移行してplayストア版とKindleFire版をボタン一つでビルドできるようになったので大変満足です。
— 竹内裕昭🐧 (@takke) July 3, 2014
それから7年ほど経ち、ソースコードが巨大化してマルチモジュール構成になり、Flavor
の切替だけで10秒くらいかかるようになり、(クラス名・メソッド名の変更といった)Android Studio のリファクタリング機能が「現在の Flavor にしか適用されない」といったつらみがきつくなってきていると薄々感じていた頃、Flavor
はもうすっかり時代遅れであることに気づかされました。
Flavorの作り分けがだんだんしんどくなってきた。普通なら1つのクラスに書く部分が Wrapper/Impl で2つに分かれ、Impl が Flavor 分増えるので 1+N 個になるんだけど、1つ修正したら Flavor 切り替えて N 回修正しないといけなくて、この Flavor 切替が遅いので、開発効率がほんと悪いしバグも多い。
— 竹内裕昭🐧 (@takke) October 26, 2021
flavor って実は使うと効率落ちる機能だよね
— wada811 (@wada811) October 26, 2021
切り替えない限りビルド通るかわからず
切り替えは遅い
アプリケーションモジュールを量産した方が良いよね
「アプリケーションモジュールを量産」かぁ、なるほど🤔(どうやるんだろう、さっぱり分からねえ)
というわけでこの記事は Product Flavor
を複数のアプリケーションモジュールに分離・展開したお話です。
そもそも Product Flavor とは
Android Studio (Android Gradle Plugin) には Product Flavor
と Build Type
という機能があり、テスト用、本番用などでアプリのビルド設定・ソースコードを切り替えることができます。
Build Type
はおなじみの debug
や release
という分類で、Flavor
は production
や develop
といった分類で使うことが多いようです。
この Flavor
と Build Type
を組み合わせたものが Build Variant
で、Android Studio の Build Variants
パネルから変更することができます。
うちのアプリの場合は
-
Flavor
:free
,kindle
,premium
-
Build Type
:debug
,release
なので、
-
Build Variants
:freeDebug
,freeRelease
,kindleDebug
,kindleRelease
,premiumDebug
,premiumRelease
の計6種類あります。
(ということで当時のソースを開いてみるとさらに(わけあって) dimension
も使っていましたが割愛します)
android {
flavorDimensions("tier", "edition")
productFlavors {
// tier
tp1 {
dimension "tier"
isDefault = true
}
tp2 {
dimension "tier"
}
// edition
free {
dimension "edition"
applicationId "com.twitpane"
manifestPlaceholders = [oauthCallbackScheme: "twitpane"]
isDefault = true
}
kindle {
dimension "edition"
applicationId "com.twitpane.kindle"
manifestPlaceholders = [oauthCallbackScheme: "twitpanekindle"]
}
premium {
dimension "edition"
applicationId "com.twitpane.premium"
manifestPlaceholders = [oauthCallbackScheme: "twitpanepremium"]
}
}
Build Variants
パネルはこんな感じ↓
複数のアプリケーションモジュールとは?
モジュールには主に下記があります。
- Application Module
- Library Module
- Dynamic Feature Module
個人的になんとなく、1プロジェクト内には Application Module
は1つだけ存在できるんだろうと思っていましたが、全然そんなことなかったんですね。
端的に言えば
apply plugin: "com.android.application"
を指定しているモジュール(たいてい app
という名前でしょう)をコピペして、app_free
や app_kindle
といった名前にして、settings.gradle
から参照してあげれば完成です。なんだ、簡単じゃん?
...
include ':app_free'
include ":app_kindle"
include ":app_premium"
Android Studio
の Configuration パネルもこんな感じになりました↓
とはいえ、、ソースコード・リソース同士が有機的に結合しているので(それ故に Flavor
を使っているので)、そう簡単に複数アプリに展開できるわけがありません。
マルチモジュール展開と同様の苦しみがありました。
作業ログ
ここからは大雑把に作業中のツイートから抜粋していきます。
マルチアプリケーション化に手を付ける前のつぶやきから。
「flavorで作り分けしてるプロジェクト」を「複数のappに分離・展開」しようとすると、まずはflavor毎のディレクトリに配置してるソースやリソースをモジュールに展開するところから始めないといけないのかな。
— 竹内裕昭🐧 (@takke) November 1, 2021
実際にその手順でやっていきました。
config_impl モジュールの分離
現状Flavorを使ってるのはapp, main, config_implの3モジュールで、全部一気にやるのはかなり厳しいのでまずはconfig_implから別モジュールに分離してみようかな。
— 竹内裕昭🐧 (@takke) November 4, 2021
config_implモジュールの各Flavorの実装を分析しつつ、個別のモジュールに展開できるか確認してみた。いけそうな気がしてきた。 pic.twitter.com/LUVUU5imkR
— 竹内裕昭🐧 (@takke) November 4, 2021
config_impl
モジュールの各Flavorに存在しているクラスについて、大まかなロジックと参照元を表にまとめ、config_impl_free
, config_impl_kinle
, config_impl_premium
モジュールに移動していき、DI で app の各Flavorでinjectするものを切り替える、といった形で実装していきました。
だいぶ進んできた。あと1クラスでconfig_implからFlavor削除できる pic.twitter.com/mq39eGxRWe
— 竹内裕昭🐧 (@takke) November 4, 2021
よし、全て個別のモジュールに移動できた。各Flavorのディレクトリが空になって気持ちいい。 pic.twitter.com/kFXUiby8Yt
— 竹内裕昭🐧 (@takke) November 4, 2021
config_impl* から Flavor を消せた。次はmainを。それにしてもモジュール多いな。 pic.twitter.com/UyGlqb8uXe
— 竹内裕昭🐧 (@takke) November 4, 2021
main モジュールの分離
次は main
モジュールを main_free
, main_kindle
, main_premium
に分離していく。
図にするとこんな感じ。
次はmainモジュールなんだけど実に手強い。定数とかリソースとかManifestとかどう展開していこうかな。まずはできるところからサクッと進めていきたいが。 pic.twitter.com/Wqe5wImdtK
— 竹内裕昭🐧 (@takke) November 4, 2021
mainモジュールでFlavor分けしてるクラスとリソースの用途などを分析してみた。若干クセはありそうだけど愚直にやっていけばモジュールに展開できそうな予感。 pic.twitter.com/ZehLRTOEff
— 竹内裕昭🐧 (@takke) November 4, 2021
mainモジュールのファイルのうち、リソース系は移動できた。文字列は競合問題があるのでいったんアプリケーションモジュールのFlavorに移動した。
— 竹内裕昭🐧 (@takke) November 4, 2021
mainモジュールからFlavor消せる! pic.twitter.com/TH4k3wyBe2
— 竹内裕昭🐧 (@takke) November 5, 2021
消した。あとは app を複数に分割するのみ。
— 竹内裕昭🐧 (@takke) November 5, 2021
app モジュールの分離
いよいよ本丸の app モジュールの分離。
やりたいことは下記の図のような感じで、app
モジュール内の main
ディレクトリ配下を app_common
モジュールに抽出し、各Flavor毎に app_free
, app_kindle
, app_premium
に分離する流れ。
app の Product Flavor もアプリとモジュールに展開した図(一番下)。最初は1アプリ+1モジュールだったのが3アプリ+5モジュールに激増してて複雑になってるけど、開発効率的にはその分の恩恵があるんだぞという話。 pic.twitter.com/kHD4ltGd0V
— 竹内裕昭🐧 (@takke) November 6, 2021
appのFlavor展開、コピペしていいなら秒で終わるんだけどそういうわけにはいかないし、設定ファイルとかどう共通化すればいいのか試行錯誤してたら無限に時間消費した
— 竹内裕昭🐧 (@takke) November 5, 2021
app の各ファイルの展開方法を検討した。まずはapp_commonにちょっとずつ移動していくかな。 pic.twitter.com/yb3CH9iFy6
— 竹内裕昭🐧 (@takke) November 6, 2021
単純に追い出せるファイルはサクッと app_common に移動した。App もこれだけ短くなればコピペも許せるかな。 pic.twitter.com/s3zRv5SOjM
— 竹内裕昭🐧 (@takke) November 6, 2021
app/build.gradle を別のファイルに切り出そうとしてるけど、別のファイルだと https://t.co/aTMUmterJY.OutputFile にアクセスできなくて sync できなくなっちゃうな。 pic.twitter.com/IBe3OtxSBo
— 竹内裕昭🐧 (@takke) November 7, 2021
getFilter(https://t.co/aTMUmterJY.OutputFile.ABI) を getFilter("ABI") と書くようにした(よくない)
— 竹内裕昭🐧 (@takke) November 7, 2021
超長かった app/build.gradle をここまで短くできた pic.twitter.com/vbaEwXkSDu
— 竹内裕昭🐧 (@takke) November 7, 2021
まずは app_kindle を分離してみた。いい感じ。 pic.twitter.com/Ddl0BT5EOg
— 竹内裕昭🐧 (@takke) November 7, 2021
ついに Product Flavor がなくなった!!対象アプリを切り替えられるぞ。やったね。 pic.twitter.com/LYhKStDrYi
— 竹内裕昭🐧 (@takke) November 7, 2021
そしてまずは KoinProductFlavorModule を KoinPremiumModule などにリネームしてたんだけど Flavor 切替をせずにちゃんと該当するソースコードだけ書き換わるのは革命的だわ。ほんと頑張って良かった。
— 竹内裕昭🐧 (@takke) November 7, 2021
app_free, app_kindle, app_premium の build.gradle がこれだけ短くなった。inject するものが違うのでその分だけ implementation が違う。 pic.twitter.com/VdbAeIwpON
— 竹内裕昭🐧 (@takke) November 8, 2021
ところで
ようやく気づいたんだけどほとんどのAndroidアプリってProductFlavorなんて使ってないんだよな。ProductFlavorを複数アプリ+モジュールに展開するやり方なんてググっても出てこないわけだ。実際にやるべきことはモジュール分割と一緒だし。超めんどくさいだけで。
— 竹内裕昭🐧 (@takke) November 5, 2021
参考URL
-
法人化していますが実質開発メンバー1人の個人開発です。 ↩