Skip to content

UI の構成 ​

これらのルールは、feature をどう分け、それぞれの部分が何を持ってよいかを決めます。PulsePlatform の中心です。ライブラリには役割ごとに部品が 1 つずつあり、ルールはどの部品も決まった場所にあるかを検査します。デスクトップのアプリは assertPulseMviConventions()、モバイルのアプリは assertPulseMobileConventions() で実行します。State、メッセージ、Section、Component、Preview のルールはどちらも同じで、違うのは feature の配置だけです。失敗すると、宣言やファイルの名前と直し方が示されます。

State とメッセージ ​

これらのルールは、デスクトップでもモバイルでも同じです。

検査ルール理由
State は data 型PulseState は data class または data objectStateFlow は直前と等しい値を流さず、Compose は引数が等しい Composable をスキップする。equals がないと更新ごとに画面全体が再コンポジションされる
メッセージは sealed classAction・Event・Broadcast・Unicast は sealed class、その中の各ケースは data class か data object。ケースが 1 つなら単独の data class か data object でもよく、interface は使わないどれも when で処理される。sealed にしてあればケースの追加がコンパイルエラーになり、data にしてあればケースが値で比較・表示される
State とメッセージは Contracts に置くPulseState・Action・Event・Broadcast・Unicast は、ViewModel か Container と同じパッケージの …Contracts.kt にあるViewModel や Container が取り決める型なので、使うクラスの隣の 1 ファイルで見つかるようにする。PulseAreaViewModel なら PulseAreaContracts.kt、PulseCountContainer なら PulseCountContracts.kt
名前は基底クラスに従うPulseViewModel は …ViewModel、PulseContainer は …Containerコンパイラにとってはどちらも ViewModel のサブクラスなので、どちらなのかを示すのは名前だけ
データは State に置くViewModel と Container は var プロパティを持たないState の外の var は変更しても emission が起きず、何も再コンポジションされない

デスクトップ ​

feature は 4 つの役割に分けます。Host はルートが表示するもので、ViewModel と Container を生成し、Content を配置します。Content は 1 つの ViewModel が受け持つ部分で、その ViewModel を PulseContent で束ねます。Section は State とコールバックから画面の一部を描き、Component は小さな部品です。Section と Component はどちらも純粋な Composable で、違うのは役割だけです。

どれに当たるかはパッケージで決まるので、Host 以外の名前は自由です。feature のルートは content パッケージを持つパッケージで、そこに Host(…Host という名前のファイル)、Container、Container の Contracts を置きます。Content は content の下にそれぞれのパッケージを持ち、ViewModel と Contracts をその隣に置きます。Section と Component は、feature のルートにも、それを使う Content の中にも置けます。ルートの下に置けるパッケージは、この 3 つだけです。stringResource でラベルを読むような、feature 全体で使う純粋な関数は component に置きます。値を返すだけの Composable は View ではないので、Preview はいりません。

count/
├── PulseCountHost.kt          # Host
├── PulseCountContainer.kt
├── PulseCountContracts.kt     # Container の Broadcast と Unicast
├── component/                 # feature 全体で使う Component
│   └── PulseCountHeader.kt
└── content/
    └── area/
        ├── PulseAreaContent.kt    # Content
        ├── PulseAreaViewModel.kt
        ├── PulseAreaContracts.kt  # ViewModel の State・Action・Event
        ├── section/               # この Content の Section
        │   └── PulseAreaSection.kt
        └── component/             # この Content の Component
            └── PulseAreaCell.kt
検査ルール理由
ViewModel は Content に置くPulseViewModel は content の下で、section と component の外にあるViewModel は 1 つの Content を受け持つので、それを束ねる Composable の隣に置き、下の純粋な部分からは見えないようにする
Container は feature のルートに置くPulseContainer は content を持つパッケージにあるContainer は feature のすべての Content をまとめるので、それを生成する Host の隣に置く
Host は Content を並べるfeature のルートにある Composable は …Host という名前のファイルにあり、PulseContent を呼ばないContent ではなく Host で束ねると、Host がすべてを知るファイルに育つ
feature のパッケージは決まっているfeature の中のファイルは、ルートか、content・section・component のどれかの下にある。build/generated の下の生成コードは対象外text のような独自のパッケージは、誰も決めていない置き場所になる
Content は ViewModel を束ねるcontent の下で section と component の外にある Composable を持つファイルは PulseContent を呼ぶViewModel と UI が出会うのは Content だけ。何も束ねていないファイルは、置き場所を間違えた Section
Section と Component は純粋section と component の下の Composable は …ViewModel や …Container を引数に取らず、PulseContent も PulseHost も呼ばないデータとコールバックだけで済むから、単体で Preview でき、使い回せる
Section と Component には Preview があるsection と component の下の public な Composable には、対応する @Preview がある純粋な Composable は Preview があれば単体で確認でき、壊れたときに最初に気づける

モバイル ​

mvi-mobile で作ったアプリでは、assertPulseMviConventions() の代わりに assertPulseMobileConventions() を呼びます。スマートフォンは一度に 1 つの画面を表示し、mvi-mobile には Container がないので、feature は Screen 1 つと ViewModel 1 つです。Host も content パッケージもありません。feature のルートは ViewModel があるパッケージです。Screen も同じパッケージの …Screen という名前のファイルに置き、PulseContent で ViewModel を結びつけます。Section と Component はデスクトップと同じで、ルートの下に置けるパッケージは section と component だけです。 表の配置の検査がデスクトップのものに替わり、このページのほかのルールはそのまま当てはまります。

counter/
├── CounterScreen.kt       # Screen: CounterViewModel を PulseContent で結びつける
├── CounterViewModel.kt
├── CounterContracts.kt    # ViewModel の State、Action、Event
├── section/
│   └── CounterSection.kt
└── component/
    └── CounterBadge.kt
検査ルール理由
feature の ViewModel は 1 つPulseViewModel は section・component・content の外にあり、同じパッケージにほかの ViewModel がない1 つの画面には ViewModel が 1 つ。2 つ目は、自分の feature を持たない 2 つ目の画面
Screen が ViewModel を結びつけるfeature のルートにある Composable は …Screen という名前のファイルにあって PulseContent を呼び、どの ViewModel の隣にもそういう Screen があるScreen はルートが表示するもので、ViewModel と UI が出会う唯一の場所
feature には決まったパッケージだけfeature の中のファイルは、ルートか section・component の下にある。ビルドが生成したコードは対象外独自のパッケージは、誰も決めていない置き場所が 1 つ増えるだけ

Preview ​

Preview は名前で Section や Component と対応付けます。<Name>_Preview、または特定の状態を示す <Name>_Preview_<条件> です。1 つあれば十分で、状態がいくつもあるものは条件ごとに置けます。それ以外の名前の Preview は数えないので、Composable だけリネームして Preview が古い名前のまま残った場合も検出できます。

PreviewPulseTextButton に対応するか
PulseTextButton_Previewする
PulseTextButton_Preview_Disabledする
PulseTextButton_Preview_Disabled_Darkする
PulseTextButtonPreviewしない

次のステップ ​

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