STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

Android処理切り替え大全

はじめに

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

ビルドタイプやフレーバーごとに値を変えることもできるので、詳しくは公式ドキュメントを参照ください。

developer.android.com

処理を切り替える

BuildConfigstaticフィールドをもった普通のJavaクラスなのでどこからでも参照できます。

たとえばBuildConfig.DEBUGを参照することにより、build.gradleで定義したdebuggableの設定に応じて処理を切り替えることができます。

if (BuildConfig.DEBUG) {
    // debuggable = true のときの処理
} else {
    // debuggable = false のときの処理
}

いろいろ応用できそうですね。

ソースセットによる切り替え

通常アプリでコードを書く場合src/main/javaにソースコードを置くと思います。

Androidではビルドタイプやプロダクトフレーバーという仕組みがありますが、これらに応じたディレクトリを切ることができます。

たとえば debugreleaseという2つのビルドタイプがあるとします。

android {
    buildTypes {
        getByName("debug") { }
        getByName("release") { }
    }
}

ここで「debugreleaseで処理を切り替えたいんだよな......」と思ったら、まずビルドタイプ名に応じたディレクトリを作ります。

- 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"

ソースセットを切り替えることで、実行時ではなくビルド時に処理(というかソース)を切り替えてしまうことができます。 詳細については公式ドキュメントを読んでみてください。

developer.android.com

Daggerによる切り替え

これまでの2つはAGPの仕組みを使った切り替えでしたが、このセクションではDIライブラリであるDaggerによる処理の切り替えをご紹介します。

github.com

以下の説明では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芸は可読性、メンテナンス性の低下に繋がる可能性があるので、ご利用は計画的に)。

ライブラリを切り替える

これだけ少し毛色が異なりますが、ビルドバリアントごとにライブラリの依存関係を切り替えることができます。

たとえば debugrelease というビルドタイプあったとして、「あるライブラリをdebugビルドには含めたいが、releaseビルドには含めたくない」というケースでは build.gradle(.kts) で以下のように依存関係を設定します。

dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:x.y.z'
}

debugImplementationとあるように、この記述によって debugビルド時にのみ LeakCanary が含まれるようになります。

また、ライブラリによっては同じインターフェイスで実装が空になった no-op なライブラリも提供されている場合もあり、

dependencies {
    debugImplementation 'hoge:huga:x.y.z'
    releaseImplementation 'hoge:huga-noop:x.y.z'
}

と記述することで、releaseビルドでは空実装のライブラリに依存しつつも、バリアントによらずに同一のインターフェイスでライブラリを利用できます。

たとえばデバッグライブラリである Flipper は no-op なライブラリも提供しているようです。

fbflipper.com

なお、依存関係の詳しい定義方法については公式ドキュメントをご覧ください。

developer.android.com

おわりに

4つの切り替え方法について見てきました。

個人的には、

  • かんたんな処理分岐やバリアントによって値を変えるケースであれば BuildConfig
  • クラスなどの比較的大きな単位でごそっと処理を切り替えるケースであればソースセットでの切り替え
  • ライブラリごと切り替えるなら依存関係で制御
  • ソースセットでは対処できないケースで Dagger による切り替え

を検討すると良いのではないかと考えています。

Daggerの方法を適用する場面はあまりないかもしれませんが「Daggerによって処理を切り替えられる」ことを知っているだけでも将来なにかの役に立つかもしれません。*3

以上、Androidにおけるいろいろな処理切り替え手法でした!

*1:このあたりは設定で挙動を変更できます。

*2:わたしが携わっている STORES 決済 アプリ/SDKがまさにこのようなモジュール構成になっています。

*3:STORSE 決済 アプリ/SDK ではDaggerによって処理の切り替えを行っている部分があります。