Product Flavor を複数のアプリモジュールに展開したお話

# はじめに

Android用のTwitterクライアントやmixiビューアといった個人開発アプリ[^1]をいくつか開発しています。

[^1]: 法人化していますが実質開発メンバー1人の個人開発です。


Twitterアプリの TwitPane は [無料版](https://play.google.com/store/apps/details?id=com.twitpane&referrer=twitpane.com)、[有料版](https://play.google.com/store/apps/details?id=com.twitpane.premium&referrer=twitpane.com)、[Kindle版](https://www.amazon.co.jp/takke-TwitPane-for-KindleFire/dp/B00LI9AWTG) という3種類を同一ソースから生成しています。

2013年頃から開発しているので `Eclipse+ADT` 時代にどうやっていたのかはもう既に覚えていませんが、`Android Studio` に移行して `Product Flavor`(以下 `Flavor`) で気軽に作り分けできるようになったときはずいぶん感動した記憶があります。


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AndroidStudioに移行してplayストア版とKindleFire版をボタン一つでビルドできるようになったので大変満足です。</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/484817607518728192?ref_src=twsrc%5Etfw">July 3, 2014</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


それから7年ほど経ち、ソースコードが巨大化してマルチモジュール構成になり、`Flavor` の切替だけで10秒くらいかかるようになり、(クラス名・メソッド名の変更といった)Android Studio のリファクタリング機能が「現在の Flavor にしか適用されない」といったつらみがきつくなってきていると薄々感じていた頃、`Flavor` はもうすっかり時代遅れであることに気づかされました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Flavorの作り分けがだんだんしんどくなってきた。普通なら1つのクラスに書く部分が Wrapper/Impl で2つに分かれ、Impl が Flavor 分増えるので 1+N 個になるんだけど、1つ修正したら Flavor 切り替えて N 回修正しないといけなくて、この Flavor 切替が遅いので、開発効率がほんと悪いしバグも多い。</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1452865269445980165?ref_src=twsrc%5Etfw">October 26, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">flavor って実は使うと効率落ちる機能だよね<br>切り替えない限りビルド通るかわからず<br>切り替えは遅い<br>アプリケーションモジュールを量産した方が良いよね</p>&mdash; wada811 (@wada811) <a href="https://twitter.com/wada811/status/1452875276434636806?ref_src=twsrc%5Etfw">October 26, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

「アプリケーションモジュールを量産」かぁ、なるほど🤔(どうやるんだろう、さっぱり分からねえ)

というわけでこの記事は `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` も使っていましたが割愛します)

```gradle
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` パネルはこんな感じ↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39221/80e70990-228c-4430-ae07-8fe9a5c88da8.png)


# 複数のアプリケーションモジュールとは？

モジュールには主に下記があります。

- Application Module
- Library Module
- Dynamic Feature Module


個人的になんとなく、1プロジェクト内には `Application Module` は1つだけ存在できるんだろうと思っていましたが、全然そんなことなかったんですね。

端的に言えば

```gradle:build.gradle
apply plugin: "com.android.application"
```

を指定しているモジュール(たいてい `app` という名前でしょう)をコピペして、`app_free` や `app_kindle` といった名前にして、`settings.gradle` から参照してあげれば完成です。なんだ、簡単じゃん？

```gradle:settings.gradle
...
include ':app_free'
include ":app_kindle"
include ":app_premium"
```

`Android Studio` の Configuration パネルもこんな感じになりました↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39221/40566eb0-b2dd-b9b8-c368-882389cef6a4.png)


とはいえ、、ソースコード・リソース同士が有機的に結合しているので(それ故に `Flavor` を使っているので)、そう簡単に複数アプリに展開できるわけがありません。

マルチモジュール展開と同様の苦しみがありました。


# 作業ログ

ここからは大雑把に作業中のツイートから抜粋していきます。

マルチアプリケーション化に手を付ける前のつぶやきから。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">「flavorで作り分けしてるプロジェクト」を「複数のappに分離・展開」しようとすると、まずはflavor毎のディレクトリに配置してるソースやリソースをモジュールに展開するところから始めないといけないのかな。</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1455060850255876103?ref_src=twsrc%5Etfw">November 1, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

実際にその手順でやっていきました。

## config_impl モジュールの分離

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">現状Flavorを使ってるのはapp, main, config_implの3モジュールで、全部一気にやるのはかなり厳しいのでまずはconfig_implから別モジュールに分離してみようかな。</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456086862699773954?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">config_implモジュールの各Flavorの実装を分析しつつ、個別のモジュールに展開できるか確認してみた。いけそうな気がしてきた。 <a href="https://t.co/LUVUU5imkR">pic.twitter.com/LUVUU5imkR</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456109894369615872?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

`config_impl` モジュールの各Flavorに存在しているクラスについて、大まかなロジックと参照元を表にまとめ、`config_impl_free`, `config_impl_kinle`, `config_impl_premium` モジュールに移動していき、DI で app の各Flavorでinjectするものを切り替える、といった形で実装していきました。


<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">だいぶ進んできた。あと1クラスでconfig_implからFlavor削除できる <a href="https://t.co/mq39eGxRWe">pic.twitter.com/mq39eGxRWe</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456161508136652806?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">よし、全て個別のモジュールに移動できた。各Flavorのディレクトリが空になって気持ちいい。 <a href="https://t.co/kFXUiby8Yt">pic.twitter.com/kFXUiby8Yt</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456168170943778819?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">config_impl* から Flavor を消せた。次はmainを。それにしてもモジュール多いな。 <a href="https://t.co/UyGlqb8uXe">pic.twitter.com/UyGlqb8uXe</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456170089145794565?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## main モジュールの分離

次は `main` モジュールを `main_free`, `main_kindle`, `main_premium` に分離していく。

図にするとこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39221/b63a9702-cb5a-7f73-98ae-7827d2bc8a5d.png)



<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">次はmainモジュールなんだけど実に手強い。定数とかリソースとかManifestとかどう展開していこうかな。まずはできるところからサクッと進めていきたいが。 <a href="https://t.co/Wqe5wImdtK">pic.twitter.com/Wqe5wImdtK</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456178658607984640?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">mainモジュールでFlavor分けしてるクラスとリソースの用途などを分析してみた。若干クセはありそうだけど愚直にやっていけばモジュールに展開できそうな予感。 <a href="https://t.co/ZehLRTOEff">pic.twitter.com/ZehLRTOEff</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456253902941081601?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">mainモジュールのファイルのうち、リソース系は移動できた。文字列は競合問題があるのでいったんアプリケーションモジュールのFlavorに移動した。</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456276936271925255?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">mainモジュールからFlavor消せる！ <a href="https://t.co/TH4k3wyBe2">pic.twitter.com/TH4k3wyBe2</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456485394870984704?ref_src=twsrc%5Etfw">November 5, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">消した。あとは app を複数に分割するのみ。</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456486592290324485?ref_src=twsrc%5Etfw">November 5, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



## app モジュールの分離

いよいよ本丸の app モジュールの分離。

やりたいことは下記の図のような感じで、`app` モジュール内の `main` ディレクトリ配下を `app_common` モジュールに抽出し、各Flavor毎に `app_free`, `app_kindle`, `app_premium` に分離する流れ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39221/7824246e-3047-7f4a-9c20-b42f5dbce2c0.png)


<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">app の Product Flavor もアプリとモジュールに展開した図(一番下)。最初は1アプリ+1モジュールだったのが3アプリ+5モジュールに激増してて複雑になってるけど、開発効率的にはその分の恩恵があるんだぞという話。 <a href="https://t.co/kHD4ltGd0V">pic.twitter.com/kHD4ltGd0V</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456897586359533571?ref_src=twsrc%5Etfw">November 6, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">appのFlavor展開、コピペしていいなら秒で終わるんだけどそういうわけにはいかないし、設定ファイルとかどう共通化すればいいのか試行錯誤してたら無限に時間消費した</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456584472296521728?ref_src=twsrc%5Etfw">November 5, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">app の各ファイルの展開方法を検討した。まずはapp_commonにちょっとずつ移動していくかな。 <a href="https://t.co/yb3CH9iFy6">pic.twitter.com/yb3CH9iFy6</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456940718195298307?ref_src=twsrc%5Etfw">November 6, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">単純に追い出せるファイルはサクッと app_common に移動した。App もこれだけ短くなればコピペも許せるかな。 <a href="https://t.co/s3zRv5SOjM">pic.twitter.com/s3zRv5SOjM</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456951338890252289?ref_src=twsrc%5Etfw">November 6, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">app/build.gradle を別のファイルに切り出そうとしてるけど、別のファイルだと <a href="https://t.co/aTMUmterJY">https://t.co/aTMUmterJY</a>.OutputFile にアクセスできなくて sync できなくなっちゃうな。 <a href="https://t.co/IBe3OtxSBo">pic.twitter.com/IBe3OtxSBo</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1457155766461165578?ref_src=twsrc%5Etfw">November 7, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">getFilter(<a href="https://t.co/aTMUmterJY">https://t.co/aTMUmterJY</a>.OutputFile.ABI) を getFilter(&quot;ABI&quot;) と書くようにした（よくない）</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1457162664044498946?ref_src=twsrc%5Etfw">November 7, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>




<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">超長かった app/build.gradle をここまで短くできた <a href="https://t.co/vbaEwXkSDu">pic.twitter.com/vbaEwXkSDu</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1457165282154201092?ref_src=twsrc%5Etfw">November 7, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">まずは app_kindle を分離してみた。いい感じ。 <a href="https://t.co/Ddl0BT5EOg">pic.twitter.com/Ddl0BT5EOg</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1457192800911429639?ref_src=twsrc%5Etfw">November 7, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">ついに Product Flavor がなくなった！！対象アプリを切り替えられるぞ。やったね。 <a href="https://t.co/LYhKStDrYi">pic.twitter.com/LYhKStDrYi</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1457201454561693697?ref_src=twsrc%5Etfw">November 7, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">そしてまずは KoinProductFlavorModule を KoinPremiumModule などにリネームしてたんだけど Flavor 切替をせずにちゃんと該当するソースコードだけ書き換わるのは革命的だわ。ほんと頑張って良かった。</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1457202267338133506?ref_src=twsrc%5Etfw">November 7, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">app_free, app_kindle, app_premium の build.gradle がこれだけ短くなった。inject するものが違うのでその分だけ implementation が違う。 <a href="https://t.co/VdbAeIwpON">pic.twitter.com/VdbAeIwpON</a></p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1457571597393416196?ref_src=twsrc%5Etfw">November 8, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>










# ところで

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ようやく気づいたんだけどほとんどのAndroidアプリってProductFlavorなんて使ってないんだよな。ProductFlavorを複数アプリ＋モジュールに展開するやり方なんてググっても出てこないわけだ。実際にやるべきことはモジュール分割と一緒だし。超めんどくさいだけで。</p>&mdash; 竹内裕昭🐧 (@takke) <a href="https://twitter.com/takke/status/1456463119442604035?ref_src=twsrc%5Etfw">November 5, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


# 参考URL

- [Androidアプリのビルド設定管理（Product Flavor、Build Type） \- Qiita](https://qiita.com/alt_yamamoto/items/7c9546a003984d53637f)

- [パルシィのアプリケーションモジュール分割 \- pixiv inside](https://inside.pixiv.blog/2021/02/09/180000)