第14回:Composeで“再利用できるUI”を作る設計

社員ブログ

※ChatGPTを使用して記事を作成しています。

Jetpack Composeを使い始めたばかりの頃、多くの開発者がこう考えます。

「Composableは関数なんだから、簡単に再利用できるはず」

しかし実際に開発を進めると、すぐに壁にぶつかります。

  • 別画面で使おうとしたら依存が多すぎる
  • ViewModelに依存していて使い回せない
  • 状態管理が複雑で他画面で使えない

そして気づきます。

「Composableは書くだけでは再利用できない」

今回は
Composeで再利用可能なUIを作る設計パターン
について整理します。

なぜComposableは再利用しづらくなるのか

Compose初心者が最初に書きがちなコードがあります。

@Composable
fun UserProfile(viewModel: UserViewModel) {    
    val user by viewModel.user.collectAsState()
    Column {
        Text(user.name)
        Button(onClick = {
            viewModel.followUser()
        }) {
            Text("Follow")
        }
    }
}

一見すると問題なさそうです。

しかしこのComposableは 再利用できません

なぜなら、

  • ViewModelに依存
  • 状態取得を内部で実行
  • ロジックが埋め込まれている

からです。

このUIは UserViewModel専用UI になっています。

つまり、

UI = ViewModel + 状態 + ロジック

がすべて混ざっています。

これでは再利用できません。

再利用できるUIの基本原則

Composeで再利用可能なUIを作るときの原則はシンプルです。

UIは状態とイベントだけを受け取る

つまり、

Composable = UI + State + Event

です。

ViewModelやRepositoryは登場しません。

再利用可能なUIの書き方

先ほどのコードを改善します。

@Composable
fun UserProfile(
    userName: String,
    onFollowClick: () -> Unit
) {
    Column {
        Text(userName)
        Button(onClick = onFollowClick) {
            Text("Follow")
        }
    }
}

これでどうなるでしょうか。

このUIは

  • ViewModel不要
  • Repository不要
  • 状態管理不要

になります。

つまり

  • どの画面でも使える
  • Previewで使える
  • テスト可能

になります。

ViewModelはどこに行ったのか

ViewModelは 呼び出し側 に置きます。

@Composable
fun UserProfileScreen(
    viewModel: UserViewModel = viewModel()
) {
    val user by viewModel.user.collectAsState()
    UserProfile(
        userName = user.name,
        onFollowClick = {
            viewModel.followUser()
        }
    )
}

ここで役割が分離されています。

Screen → 状態取得
UI → 表示のみ

この構造は Composeの設計パターンの基本 です。

さらに再利用性を高めるテクニック

ここからが実務で重要なポイントです。

再利用できるUIには 3つのレベル があります。

レベル1:データのみ受け取るUI

最もシンプルな形です。

@Composable
fun UserName(name: String) {
Text(name)
}

これは完全に再利用できます。

ただし、

現実のUIはもっと複雑です。

レベル2:UI Stateを受け取る

Composeでは UI Stateをまとめて渡す設計 がよく使われます。

data class UserProfileUiState(
val name: String,
val isFollowing: Boolean
)

Composableはこれを受け取ります。

@Composable
fun UserProfile(
    state: UserProfileUiState,
    onFollowClick: () -> Unit
) {
    Column {
        Text(state.name)
        Button(onClick = onFollowClick) {
            Text(
                if (state.isFollowing) "Following"
                else "Follow"
            )
        }
    }
}

メリットは大きいです。

  • 引数が整理される
  • UI状態が一目でわかる
  • 拡張しやすい

レベル3:Slot API

Composeの強力な再利用パターンが Slot API です。

例えば Scaffold はこの構造です。

Scaffold(
topBar = { TopAppBar(...) },
content = { ... }
)

自分でも同じことができます。

@Composable
fun ProfileCard(
    header: @Composable () -> Unit,
    content: @Composable () -> Unit
) {
    Card {
        header()
        Spacer(Modifier.height(8.dp))
        content()
    }
}

使用例

ProfileCard(
header = {
Text("User Profile")
},
content = {
Text("Profile Content")
}
)

これにより

UI構造を再利用しながら中身を差し替える

ことができます。

再利用UIを壊すアンチパターン

実務でよくある失敗を紹介します。

アンチパターン① ViewModel依存

@Composable
fun UserCard(viewModel: UserViewModel)

これは再利用不能です。

アンチパターン② collectAsState inside UI

val user by viewModel.user.collectAsState()

UIの責務ではありません。

アンチパターン③ MutableStateを渡す

@Composable
fun UserName(name: MutableState<String>)

これは危険です。

UIが 状態を書き換えられる ようになります。

推奨は 値 + イベント です。

Compose再利用設計の黄金ルール

実務で役立つルールをまとめます。

① UIはViewModelを知らない

Composable → ViewModel禁止

② UIはStateだけ受け取る

Composable(state, event)

③ 状態取得はScreenが担当

Screen → ViewModel
UI → 描画

なぜComposeは再利用設計が重要なのか

従来のXML UIでは、

View + findViewById

という構造でした。

UIは再利用より 画面単位設計 でした。

しかしComposeは違います。

UI = 関数

だからこそ、

設計しないと再利用できない

という世界になりました。

まとめ

Composeで再利用可能なUIを作るためには、次の原則が重要です。

① UIは状態とイベントだけ受け取る

Composable(
state,
onEvent
)

② ViewModelはScreenに置く

Screen → ViewModel
UI → 描画

③ Slot APIで拡張可能にする

Composable(
header = {},
content = {}
)

Composeは「関数UI」です。

しかし、

関数だからといって自動的に再利用できるわけではありません。

再利用できるUIを作るためには、

  • 状態設計
  • 責務分離
  • Slot API

といった 設計思想 が必要です。

そしてこの設計を理解すると、

Composeのコードは

驚くほどシンプルで拡張しやすくなる

のです。

タイトルとURLをコピーしました