はじめに
STORES 決済 のAndoridアプリを開発している id:n-seki です。
突然ですが、Androidアプリ開発をしていると何かしら「処理を切り替えたく」なることって多くないですか?
- 「debuggable = trueのときだけXXしたい......」
- 「バリアントAでビルドしたときはXX、 BでビルドしたときはYYという振る舞いにしたい......」
- 「(マルチモジュールにおいて)モジュールAをアーティファクトとしてビルドしたときはXX、 BをビルドしたときはYYに挙動を変えたい......」
ユースケースに従った適切な切り替え方法を知ることで「このケースではこうかな」と判断できるようになります。常に使う手法というわけではないですが、道具箱の片隅にでも置いておくといつか役立つ......かもしれません。
さあ、処理を切り替えていきましょう!
BuildConfigによる切り替え
BuildConfig
とはビルド時に生成されるクラスで、debuggable
の設定が反映されたDEBUG
という定義などがあるほか、自分で好きな定義を追加することもできます。
BuildConfigに定義を追加する
android { buildFeatures { // AGP 8.0.0 からデフォルトfalseになっているので明示的にtrueを指定 // https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes#default-changes buildConfig true } defaultConfig { buildConfigField("Integer", "BUILD_NUMBER", "12345") } }
build.gradle(.kts)
でこういう記述をしてビルドするとBuildConfig
に定義が追加されます。
public static final Integer BUILD_NUMBER = 12345
ビルドタイプやフレーバーごとに値を変えることもできるので、詳しくは公式ドキュメントを参照ください。
処理を切り替える
BuildConfig
はstatic
フィールドをもった普通のJavaクラスなのでどこからでも参照できます。
たとえばBuildConfig.DEBUG
を参照することにより、build.gradle
で定義したdebuggable
の設定に応じて処理を切り替えることができます。
if (BuildConfig.DEBUG) { // debuggable = true のときの処理 } else { // debuggable = false のときの処理 }
いろいろ応用できそうですね。
ソースセットによる切り替え
通常アプリでコードを書く場合src/main/java
にソースコードを置くと思います。
Androidではビルドタイプやプロダクトフレーバーという仕組みがありますが、これらに応じたディレクトリを切ることができます。
たとえば debug
とrelease
という2つのビルドタイプがあるとします。
android { buildTypes { getByName("debug") { } getByName("release") { } } }
ここで「debug
とrelease
で処理を切り替えたいんだよな......」と思ったら、まずビルドタイプ名に応じたディレクトリを作ります。
- src - main - java - debug - java - release - java
それぞれファイルを作成して、配置します。
src/debug/java/Constants.kt
object Constants { const val ENV = "debug" }
src/release/java/Constants.kt
object Constants { const val ENV = "release" }
- src - main - java - debug - java - Constants.kt - release - java - Constants.kt
main
にあるクラスからConstants
を参照できますが、ビルドバリアントdebug
でビルドしたときにはsrc/debug/
の、release
でビルドしたときはsrc/release
のソースコードが参照されます。*1
Log.d("HogeHoge", "env=${Constants.ENV}") // debugなら”env=debug", releaseなら"env=release"
ソースセットを切り替えることで、実行時ではなくビルド時に処理(というかソース)を切り替えてしまうことができます。 詳細については公式ドキュメントを読んでみてください。
Daggerによる切り替え
これまでの2つはAGPの仕組みを使った切り替えでしたが、このセクションではDIライブラリであるDagger
による処理の切り替えをご紹介します。
以下の説明ではHiltを使わないPureなDaggerを使いますが、Hiltでも同じことができると思います。
ユースケース
多くのケースは上記2つの方法で対処できると思いますが、モジュール単位で処理をごっそり変える場合には有効な手段かなと思います。
マルチモジュール構成のプロジェクトを想像してください。3つのモジュールがあります。
- app
- アプリケーションモジュール。ビルドすると(リリース用)APKやAABになる。
- lib
- ライブラリモジュール。ビルドすると(公開用)aarになる。
- base
- ライブラリモジュール。appとlibに共通する処理が実装されており、appとlibから参照される。
1つのリポジトリでビルドするモジュールを変えることによって複数のアーティファクトを生成する構成です。*2
ここで「アプリとライブラリに共通する機能だが、それぞれで少しだけ挙動を変えたい」場合にどうするのか良いでしょうか?
実行環境を判定できる何かしらの実装をした上で、baseモジュールに条件分岐を書くのは1つの解だと思います。
if (アプリだったら) { // アプリの処理 } else { // ライブラリの処理 }
Daggerを使うことでよりダイナミックに、かつ柔軟に処理を切り替えることができます。
想定ケース
こんなケースを考えましょう。
// baseモジュール interface PaymentService { fun startPayment() } // baseモジュール // serviceの実装をアプリとライブラリとで切り替えたい! class STORESPayments @Inject constructor(private val service: PaymentService) { fun startPayment() { service.startPayment() } }
STORESPayments
という共通クラスがPaymentService
インターフェイスに依存しています。
ここでPaymentService
の実装をアプリとライブラリで切り替えたい、とします。
実装
モジュール間で異なる実装を提供したいので、インターフェイスの実装クラスをそれぞれ作ります。
app
モジュールの実装。
class AppPaymentService : PaymentService { override fun startPayment() { Log.d("AppPaymentService", "start payment from Application") } }
lib
モジュールの実装。
class LibPaymentService : PaymentService { override fun startPayment() { Log.d("LibPaymentService", "start payment from library") } }
あとはこれをDaggerで配ればよいですが、この記事ではシンプルに、
- アプリ(app)をビルドしたときのDaggerグラフ
- ライブラリ(lib)をビルドしたときのDaggerグラフ
2つの異なるDaggerのグラフを作る方法を紹介します。
まずはapp
モジュールから。
@Module interface AppModule { @Binds fun bindPaymentService(appPaymentService: AppPaymentService): PaymentService } @Component(modules = [AppModule::class]) @Singleton interface AppComponent { fun inject(hogehoge: HogeHoge) }
つぎにlib
モジュール。
@Module interface LibModule { @Binds fun bindPaymentService(libPaymentService: LibPaymentService): PaymentService } @Component(modules = [LibModule::class]) @Singleton interface LibComponent { fun storesPayments(): STORESPayments }
Component
の実装がこのとおりである必要はもちろんなくて、大事なのはService
の異なる実装をそれぞれDIする、というところです。
もう一度base
モジュールのSTORESPayments
の実装を見てみましょう。
class STORESPayments @Inject constructor(private val service: PaymentService) { fun startPayment() { service.startPayment() } }
このクラスにはPaymentService
が注入される必要がありますが、
- アプリ(app)をビルドしたときには
AppPaymentService
- ライブラリ(lib)をビルドしたときには
LibPaymentService
が注入されることになります。
上記の例だとこんな感じで使えます。
同じ STORESPayments::startPayment
を呼び出していますが、内部で依存しているService
の実装が切り替わっているため挙動を変えることができました。
// appモジュール @Inject lateinit var storesPayments: STORESPayments DaggerAppComponent.create().inject(this) storesPayments.startPayment() // AppPaymentService: start payment from Application // libモジュール val storesPayments = DaggerLibComponent.create().storesPayments() storesPayments.startPayment() // LibPaymentService: start payment from library
このとおりの実装である必要はないのはもちろん、実装を工夫することでより複雑なケースにも対応できます(が、高度なDagger芸は可読性、メンテナンス性の低下に繋がる可能性があるので、ご利用は計画的に)。
ライブラリを切り替える
これだけ少し毛色が異なりますが、ビルドバリアントごとにライブラリの依存関係を切り替えることができます。
たとえば debug
と release
というビルドタイプあったとして、「あるライブラリをdebugビルドには含めたいが、releaseビルドには含めたくない」というケースでは build.gradle(.kts)
で以下のように依存関係を設定します。
dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:x.y.z' }
debug
Implementationとあるように、この記述によって debug
ビルド時にのみ LeakCanary
が含まれるようになります。
また、ライブラリによっては同じインターフェイスで実装が空になった no-op
なライブラリも提供されている場合もあり、
dependencies { debugImplementation 'hoge:huga:x.y.z' releaseImplementation 'hoge:huga-noop:x.y.z' }
と記述することで、releaseビルドでは空実装のライブラリに依存しつつも、バリアントによらずに同一のインターフェイスでライブラリを利用できます。
たとえばデバッグライブラリである Flipper は no-op
なライブラリも提供しているようです。
なお、依存関係の詳しい定義方法については公式ドキュメントをご覧ください。
おわりに
4つの切り替え方法について見てきました。
個人的には、
- かんたんな処理分岐やバリアントによって値を変えるケースであれば
BuildConfig
- クラスなどの比較的大きな単位でごそっと処理を切り替えるケースであればソースセットでの切り替え
- ライブラリごと切り替えるなら依存関係で制御
- ソースセットでは対処できないケースで
Dagger
による切り替え
を検討すると良いのではないかと考えています。
Dagger
の方法を適用する場面はあまりないかもしれませんが「Dagger
によって処理を切り替えられる」ことを知っているだけでも将来なにかの役に立つかもしれません。*3
以上、Androidにおけるいろいろな処理切り替え手法でした!