entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

パルシィのアプリケーションモジュール分割

ピクシブ株式会社で主にアプリ開発を担当しているmakunです。普段は、パルシィ(Palcy)のAndroidアプリ版(以降palcy-androidと表記)の開発をしています。

play.google.com

以前には次のような記事を書いています。

inside.pixiv.blog

今回は、パルシィのアプリケーション分割についてまとめました。

palcy-androidでは、開発中のアプリとリリースするアプリで設定を変更したり、リリースには含めたくないデバッグ用のコードなどがあります。なので、それらを管理するためにアプリケーションをモジュールに分割し、それぞれに設定を記述したりコードを追加できるようにしました。

具体的には、アプリケーションを開発用、リリース確認用、本番用の3つにモジュールで分割しています。モジュールはそれぞれapp_development app_staging app_production としてアプリケーションモジュールを作成し、共通する部分を modules/core/app というモジュールに定義しています。階層は次のようになっています。

palcy-android
 ├ app_development
 ├ app_staging
 ├ app_production
 └ modules/core/app

モジュール毎の役割について

modules/core/app

それぞれのモジュールで共通する処理をまとめたApplicationクラスを配置するモジュールです。次のように、各ライブラリの初期化やロガー(JakeWharton/timber) の設定などをしています。ちなみにDIのフレームワークには InsertKoinIO/koin を採用しています。

abstract class PalcyApplication : Application() {
    abstract val logTree: Timber.Tree
    ...

    @CallSuper
    override fun onCreate() {
        super.onCreate()
        ...
        Timber.plant(logTree)
        FirebaseApp.initializeApp(applicationContext)
        startKoin {
            androidLogger()
            androidContext(applicationContext)
            modules(appModule)
        }
        ...
    }
    ...
}

以下のアプリケーションモジュールではこのベースとなるクラスを継承したアプリケーションクラスを作成し利用します。

app_development

普段のアプリ開発で利用する開発用のアプリケーションモジュールです。開発用のアプリケーションでは、SharedPreferencesの値を受け取りDIのコンポーネントを変更するなどし、Retrofit2のレスポンス内容を固定したりできるようにしています。

@Suppress("unused")
class DevelopmentApplication : PalcyApplication() {
    override val logTree: Timber.Tree = DevelopmentTree()

    private val sharedPreferences: SharedPreferences
        get() = PreferenceManager.getDefaultSharedPreferences(this)

    private val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
        // 設定の変更通知を受け取りコンポーネントを再定義
        loadKoinModules(sharedPreferences.getModule(key))
    }

    private fun SharedPreferences.getModules(key: String): Module = when (key) {
        getString(R.string.key_service_comic) -> module {
            factory(override = true) {
                if (getBoolean(key, false))
                    retrofit.create(DevelopmentComicService::class.java) else
                    retrofit.create(ComicService::class.java)
            }
        }
        ...
        else -> module { }
    }

    override fun onCreate() {
        super.onCreate()
        sharedPreferences.registerOnSharedPreferenceChangeListener(listener)

        // 設定の初期値をみてコンポーネントを再定義
        loadKoinModules(listOf(
            sharedPreferences.getModule(getString(R.string.key_service_comic)),
            ...,
        ))
    }
}

また、これらの設定の値を変更するために AndroidX Preference Library を利用し簡単な設定画面を作り、それを通知からIntentで起動できるようにしています。そちらに内容に関しては今回の記事に含めませんでした。

これらの開発用のコード(設定画面や、DevelopmentComicServiceなどの開発用クラス)は app_development のモジュール内に設置するため、リリース用のビルドには含まれません。なので、Material componentsを雑に使った簡単な実装ばかりになっています。

また、Androidの新しい機能やKotlinの新しい機能も雑に使えたりして、開発が楽しく感じるため個人的には気に入っています。

app_staging

リリース前のアプリを確認するためのモジュールです。基本的な設定は本番環境と同じですが、トラッキングイベントの送信先やログの粒度が違います。

app_production

ユーザーが実際に触ることになるアプリのモジュールです。各種設定を本番用のものに設定してビルドします。versionNameversionCode はCIから設定を行い、それぞれGit Tagの名前と今までのコミット数を利用するようにしています。

build.gradleの設定

モジュールを分割すると、それぞれにbuild.gradleを配置する必要がありますが、毎回同じ記述を書いたりすると管理場所が増えるため、どこかにまとめておく必要がありました。そこで、つぎのようなファイルを作成し palcy-android/gradle 配下に設置しました。

file name apply
android.gradle kotlin-**
application.gradle com.android.application
module.gradle com.android.library

それぞれの役割は次のようになっています

android.gradle

androidブロックに記述する基本的な内容をまとめたファイルです。またすべてのモジュールにおいて利用するモジュールも読み込んでいます。

apply plugin: "kotlin-android"
apply plugin: 'kotlin-kapt'
apply plugin: "kotlin-parcelize"
apply plugin: 'kotlinx-serialization'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 29
        ...
    }
    ...
}

dependencies { ... }

このようにプラグインの読み込みも共通したファイルに記述しておいたことで、kotlin-android-extensionsの除去なども楽に行うことができました。この android.gradle は、次に説明するモジュールで必ず読み込むことになります。

application.gradle

アプリケーションモジュールで利用するファイルです。ここで各アプリケーションに共通する処理をまとめておきます。versionCodeversionName は、CIでビルドした際に設定できるよう環境変数から読み込むようにし、デフォルトでは1に設定していて普段の開発では常にその値を利用しています。また、アプリケーションモジュールが必ず必要となるモジュールも読み込むようにしています。

apply plugin: 'com.android.application'
apply from: rootProject.file("gradle/android.gradle")

project.ext {
    versionCode = System.getenv("VERSION_CODE") ?: "1"
    versionName = "${project.versionTag}"
}

android {
    defaultConfig {
        applicationId project.applicationId
        versionCode Integer.parseInt(project.versionCode)
        versionName project.versionName
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    signingConfigs { ... }
    buildTypes {
        debug { ... }
        release { ... }
    }
    ...
}

dependencies {
    implementation project(":modules:core:app")
    ...
}
...

このファイルを各アプリケーションモジュールの build.gradle で読み込み利用します。その際に、versionTagapplicationId をアプリケーション毎に設定できるようにしています。app_development の例が次になります。

project.ext {
    versionTag = "1000"
    applicationId = "jp.pxv.hogehoge"
}

apply from: rootProject.file("gradle/application.gradle")

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

module.gradle

アプリケーションモジュール以外のモジュールで必ず読み込むファイルです。とくに特別な処理はありませんが、アプリケーションモジュールと読み込むプラグインが違うため必要になります。

apply plugin: "com.android.library"
apply from: rootProject.file("gradle/android.gradle")

実際に利用すると次のようになります。

apply from: rootProject.file("gradle/module.gradle")

dependencies { ... }

アプリケーション毎の値の設定

versionCodeやversionNameの設定や、ビルド設定などは先ほどのgradleファイルで行うことができました。他にも、Retrofit2のbaseUrlや各種サービスのキーやトークンなどの設定もアプリケーション毎に行いたいです。

固定的な値はいろいろな方法で設定できますが、palcy-androidでは、stringをxmlで定義してコード内でgetStringする方法を採用しています。理由としては、必要なモジュールにだけファイルを設置するだけで良いのでCIの設定等に易しく、開発もしやすいというのがあります。

実際には次のようなファイルを作ります。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="api_base_url">https://hoge.piyo.jp/</string>
    <string name="database_prefix">development</string>
    <string name="hoge_key">hogepiyo</string>
    <string name="hoge_app_id">hogehoge_piyopiyo</string>
    <string name="hoge_app_domain">pixiv.hoge.com</string>
</resources>

このようなxmlファイルを作成し、CIでビルドする際に設置します。そうすることで、リリース用の大事な設定ファイルをリポジトリに含める必要がなくなります。ただ、開発用のアプリでは開発をスムーズに進めるため、予めxmlファイルを設置しgitignoreもしていません。理由としては、palcy-androidが自社でホストするGitLabで管理されているというのが大きいです。

また、これらの設定をコード内で扱いやすくするため、固定値をまとめて扱うためのインターフェースを定義しています。

interface PalcyConstants {
    val apiBaseUrl: String
    val databasePrefix: String
    val hogeKey: String
    val hogeAppId: String
    val hogeAppDomain: String
}

これをベースのアプリケーションでDIのコンポーネントとして定義しておけばテスト等で設定を変更しやすくなります。

val appModule = module {
    single {
        val app = androidApplication()
        obejct : PalcyConstants {
            override val apiBaseUrl: String = app.getString(R.string.api_base_url)
            override val databasePrefix: String = app.getString(R.string.database_prefix)
            override val hogeKey: String = app.getString(R.string.hoge_key)
            override val hogeAppId: String = app.getString(R.string.hoge_app_id)
            override val hogeAppDomain: String = app.getString(R.string.hoge_app_domain)
        }
    }
}

まとめ

アプリケーションを役割毎にモジュール分割したことで、アプリ全体のモジュール構成の見直しや、DI環境の整理、各設定の扱いやCIの設定なども整えることができました。さらに、デバッグ用のコードを自由に書けるなど多くの挑戦も可能となり、開発のモチベーションをあげるきっかけにもなりました。これをきっかけに、開発用の設定画面などのも整備したため、今度はそれについても紹介できたらと思います。

この記事でピクシブに興味をもった方がいれば是非、弊社で一緒に働きましょう!ピクシブ株式会社では、Androidアプリエンジニアを 新卒中途 ともに絶賛募集中です!

hrmos.co

hrmos.co

20191219012842
makun
2018年4月新卒入社。講談社と共同で開発しているパルシィというアプリのAndroid版を担当する。エンジニア採用や育成も行う。