ピクシブ株式会社で主にアプリ開発を担当しているmakunです。普段は、パルシィ(Palcy)のAndroidアプリ版(以降palcy-androidと表記)の開発をしています。
以前には次のような記事を書いています。
今回は、パルシィのアプリケーション分割についてまとめました。
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
ユーザーが実際に触ることになるアプリのモジュールです。各種設定を本番用のものに設定してビルドします。versionName
や versionCode
は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
アプリケーションモジュールで利用するファイルです。ここで各アプリケーションに共通する処理をまとめておきます。versionCode
や versionName
は、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
で読み込み利用します。その際に、versionTag
と applicationId
をアプリケーション毎に設定できるようにしています。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アプリエンジニアを 新卒、中途 ともに絶賛募集中です!