Skip to content

モジュールと Koin ​

これらのルールは、マルチモジュールのアプリの依存を一方向に保ち、組み立てを 1 か所にまとめます。すべてのファイルの import を読むので、デスクトップでもモバイルでも同じです。

assertPulseModuleConventions() はマルチモジュールのアプリを検査します。モジュール名は Now in Android にならい、依存の向きは Clean Architecture に従います。

層 ​

各モジュールはどれか 1 つの層に属します。ここに挙げた名前以外のモジュールは検査しません。

モジュール層
appすべてをつなぐアプリ本体
feature/<name>1 つの機能の Host・Content・Section・Component・ViewModel。feature/<name>/api と /impl は 1 つの feature として扱う
core/domainエンティティ、Repository の interface、UseCase(作る場合)
core/dataRepository の実装と、それが読む DataSource
core/designsystemテーマと、すべての画面で使う部品
core/uidomain のエンティティを描く、feature 間で共有する Composable
その他の core/<name>core/common や core/testing などの補助

依存の向き ​

domain が中心で、どこにも依存しません。Now in Android では domain モジュールが data モジュールに依存しますが、ここでは domain が Repository の interface を持ち、core/data がそれを実装します。feature は domain を通してだけデータに触れ、実装は app が渡します。

designsystem もどこにも依存しません。部品は文字列・数値・コールバックを受け取るので、何を表示する画面でも使えます。エンティティを受け取る Composable は core/ui に置きます。core/ui は domain と designsystem の両方を使えます。

ルール ​

依存は各ファイルの import から読み取るので、検査するのは実際に使われているコードです。

検査ルール
app に依存しないどのモジュールも app から import しない
feature は独立しているfeature はほかの feature から import しない
core は feature に依存しないcore のモジュールは feature から import しない
domain は何にも依存しないdomain は補助モジュールからだけ import する
data は domain を実装するdata は domain と補助モジュールからだけ import する
designsystem は独立しているdesignsystem は補助モジュールからだけ import する
共有 UI は描くだけ共有 UI は domain、designsystem、自身、補助モジュールからだけ import する
feature は domain 経由でデータに触れるfeature は data から何も import しない
domain はフレームワークに依存しないdomain は Compose、AndroidX Lifecycle、PulsePlatform から import しない
designsystem は種類ごとに分けるdesignsystem のファイルは、種類の名前のパッケージにある

パッケージ ​

domain と data の中では、パッケージで宣言の種類を表します。ファイルは次のどれかのパッケージに置きます。パッケージ名のどの部分が当たってもよく、com.example.core.domain.model も com.example.core.domain.model.area も model として扱います。

local の DataSource は永続化だけを担い、渡されたものを読む・書く・消すだけです。カウントを 1 増やす、値がないときをどう扱うかといった、値の変え方は Repository が決めます。

designsystem は、種類ごとに分けます。中身はすべて部品なので、component というパッケージ名では何が入っているのか分かりません。種類ごとのパッケージにすれば、ボタンがどこにあるかがすぐ分かります。ルートはモジュールのディレクトリと同じ名前のパッケージ(core/designsystem なら designsystem)で、その直下にはファイルを置きません。

各層のパッケージを表に、典型的な構成をその下に示します。

層パッケージ
domainエンティティは model、Repository の interface は repository、UseCase は usecase
dataRepository の実装は repository、DataSource は local、DataSource が保存する形は model
designsystembutton・card・theme のように種類ごとに 1 つ。component・components・widget・widgets・composable・composables・common のような何でも入る名前は使わない
core/domain/…/domain/
├── model/          # PulseArea
├── repository/     # PulseCountRepository
└── usecase/        # ReceivePulseUseCase
core/data/…/data/
├── local/          # PulseCountLocalDataSource
├── model/          # DataSource が保存する形(エンティティと異なる場合)
└── repository/     # DataStorePulseCountRepository
core/designsystem/…/designsystem/
├── button/         # PulseButton, PulseTextButton
├── card/           # PulseCard
└── theme/          # PulsePlatformTheme

デスクトップとモバイル ​

デスクトップとモバイルの両方に出すアプリは、1 つのプロダクトを 2 回描いたものではなく、2 つのプロダクトです。デスクトップのウィンドウとスマートフォンの画面は使われ方が違い、スマートフォン版に合わせたデスクトップ版は使いにくくなります。そのため、2 つの機能や画面がそろっている必要はありません。プロダクトごとに app と feature を自分のディレクトリの下に持ち、ルートの core モジュールを両方で共有します。mvi-mobile が両方に向けた 1 つのアーティファクトなので、Android と iOS は 1 つのプロダクトです。デスクトップのウィンドウの枠のように片方のプロダクトだけが使う core モジュールは、そのプロダクトの下に desktop/core/<name> として置けます。この構成を表すのが PulseModuleLayout.DesktopAndMobile です。

desktop/
├── app/                 # ウィンドウ、ナビゲーション、デスクトップの Koin モジュール
├── core/designsystem/   # 任意:デスクトップだけが使う部品
└── feature/<name>/      # Host・Content・Container:デスクトップの画面構成
mobile/
├── app/                 # Android と iOS の入口、モバイルの Koin モジュール
└── feature/<name>/      # Screen 1 つと ViewModel 1 つ:モバイルの画面構成
core/
├── domain/              # 両方のプロダクトで共有
├── data/
├── designsystem/
└── ui/

2 つのプロダクトの検査 ​

このレイアウトを渡すと、モジュールの検査に次の 3 つが加わります。モジュール名の先頭にはプロダクト名が付くので、desktop/feature/music と mobile/feature/music は別のモジュールです。assertPulseProductConventions() は、各プロダクトの feature を、そのプロダクトが使うアーティファクトの画面構成で検査します。デスクトップの feature は assertPulseMviConventions()、モバイルの feature は assertPulseMobileConventions() です。どちらに当たるかは feature の置き場所から読み取るので、テストの側でディレクトリを選ぶ必要はありません。2 つのプロダクトに同じ feature があるかは検査しません。

検査ルール
app と feature はプロダクトに属するapp と feature はすべて desktop/ か mobile/ の下にある。feature/<name> に置くと失敗する
プロダクトは互いに独立しているデスクトップはモバイルから、モバイルはデスクトップから何も import しない
共有モジュールはどちらにも属さないどちらのプロダクトにも属さないモジュールは、どちらからも import しない
kotlin
@Test
fun `follows the PulsePlatform conventions of each product`() {
    val scope = Konsist.scopeFromProduction()
    scope.assertPulseModuleConventions(PulseModuleLayout.DesktopAndMobile)
    scope.assertPulseKoinConventions(PulseModuleLayout.DesktopAndMobile)
    scope.assertPulseProductConventions()
}

別のモジュール名 ​

モジュール名は PulseModuleLayout で決まります。層ごとにパスのパターンを並べ、モジュールのディレクトリの末尾と照合します。* は 1 階層を表します。層は順に判定するので、support の広いパターンは、ほかの層が取らなかったモジュールだけを拾います。モジュールの検査と Koin の検査は、すべてレイアウトを受け取ります。

kotlin
val layout =
    PulseModuleLayout(
        app = listOf("desktop"),
        features = listOf("features/*"),
        domain = listOf("shared/domain"),
        data = listOf("shared/data"),
        sharedUi = listOf("shared/widget"),
        support = listOf("shared/*"),
        designSystem = listOf("shared/theme"),
    )

Konsist.scopeFromProduction().assertPulseModuleConventions(layout)

Koin ​

assertPulseKoinConventions() は Koin を使うアプリのためのもので、Koin を使っていないプロジェクトは通ります。制限するのは Koin モジュールの定義だけで、koinInject、getKoin、parametersOf による取得はどこでも構いません。モジュールの検査と同じ PulseModuleLayout を使います。

検査ルール理由
Koin モジュールは app に置くKoin モジュールを定義するファイル(org.koin.dsl.module か Koin Annotations の @Module を使うもの)は app モジュールにあるapp はほかのすべてを知る唯一のモジュールなので、依存の全体を 1 か所で読める。feature や core が自分で Koin モジュールを持つと、その判断の一部を app から奪う

次のステップ ​

  • 文言 — ユーザーが読む文言をどこから持ってくるか

Apache 2.0 License の下で公開されています。