eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

Androidアプリのmicro-module設計

  • サーバーサイドの世界では、micro-servicesとか流行ってるね
  • Androidの世界で、microな設計思想は取り入れられるのか?

Androidアプリのmoduleはどこまで分けられるのか?

  • Domain
  • Bridge
    • ドメインで定義したServiceの実装詳細を書く
  • Factory
    • どのブリッジを使用するか選択する
    • SQLite使う? Realm使う? そういう切り分けを行う
  • Components
  • Application
    • 最終的な成果物作成モジュール
    • google-services.jsonを組み込んだり、Crashlyticsを組み込んだり
    • Flavorでapkを分けたりする

Componentsをどのように分けるか

Activityをinternal化する

  • もちろん、関連するFragmentも全部internalにする
// こうしたい
// こうすることで、画面設計の詳細が[app_components_splash]モジュールから漏れ出さない
internal class SplashActivity : Activity() {
}
  • Kotlinコードからアクセスできないだけで、AndroidManifestに書けばちゃんと動作する

Activity間の遷移をどうするか?

  1. [app]
  2. [app_components_splash]
  3. [app_components_main]
// app/build.gradle
// [app_components_splash]と[app_components_main]を完全に非依存としたい
dependencies {
  implementation project(':app_components_splash')
  implementation project(':app_components_main')
}
  • 通常はこうしたいが
  • 無策にやると、これはできない
// SplashActivity.kt から、 MainActivity.kt は見えない!
// なぜなら、別moduleかつ非依存かつinternalで定義されているから!!
startActivity(Intent(context, MainActivity::class.java))

Component間に規約を設ける

  • app_components_main/AndroidManifest.xmlにmeta-dataを追加
  • こうすることで、Contextから拾うことができる
<?xml version="1.0" encoding="utf-8"?>
<manifest >
    <application android:theme="@style/Theme.AppCompat.NoActionBar">
        <activity
            android:name=".MainActivity">
            <meta-data
                android:name="com.eaglesakura.modules.COMPONENT_KEY"
                android:value="com.eaglesakura.modules.screen.MAIN" />
        </activity>
    </application>
</manifest>

Contextからmeta-data経由でIntentを生成する

// こんな感じでpackageInfo取得 -> activityInfo検索 -> Intent生成が行える

packageInfo = context.packageManager.getPackageInfo(
    context.packageName,
    PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_META_DATA
)

for (activityInfo in (packageInfo.activities ?: emptyArray())) {
    val componentKey = activityInfo.metaData?.getString("com.eaglesakura.modules.COMPONENT_KEY")
    if("com.eaglesakura.modules.screen.MAIN" == componentKey) {
      return Intent().also {
          it.component = ComponentName(context.packageName, activityInfo.name)
      }
    }
}

突き詰めるとどうなるか

  • 1画面1module、1service1module、1provider1module...というレベルまで細分化できる
  • [app] モジュールは、最終的なビルドフレーバーを分けたりapplication-idを確定させる程度の存在になる

利点はあるのか?

  • UIやシステムコンポーネントレベルでの依存性が無くなるため、moduleの交換が容易になる
    • 1画面だけ何か新しいライブラリを入れてみよう、といったことが行える
    • デザインの変更
      • 「この画面だけに必要なリソース」等を分けやすい
    • 特定moduleを捨てる決断が容易になる
  • module間が粗結合になるため、担当者を分けやすい
  • 実際に分けてみると、依存しているライブラリが「この画面のためだけに導入した」という感じになる
    • 意図せず使ってしまって密結合になるケースがある
    • そういったケースを防げる

欠点は何か?

  • moduleがめっちゃ増える
  • PCへの負荷が増える
    • Android Studio 3.2世代ならまあ問題ない
    • i7 7500U, RAM16GB + Ubuntu環境でそれなりに開発できる
    • 物理4コア以上でしょ、いまどき?
  • Gradleの implementation api , Kotlinの internal に対する知識が最低限必要になる
    • 何がinternalなのか、ではない
    • 何がpublicなのか に注目して設計し、それ以外は全部privateかinternalにする
  • メモリを食う
    • CircleCIのデフォルトメモリ(4GB)だと稀によく落ちる場合がある
  • res/*.xml が増える

CIのメモリ不足対処

  • どうしてもmodule数が増えるとメモリ使用量も増える
    • 予算や規模の都合でresource_classが変更できない場合
  • compileとassemble/testを分ける
# *.classを先行して生成する
./gradlew --no-daemon :app_components_main:compileDebugSources

# *.classのキャッシュを使うことで、メモリ使用量を抑える
./gradlew --no-daemon :app_components_main:testDebug

結論として、この設計は使い物になるか?

  • 今の所使えそう