Android Jetpack Compose

Jetpack Composeで作る再利用性の高い画面構造

はじめに

Jetpack Composeでアプリを開発する際、画面構造をどのように整理するかは重要な課題です。
この記事では、メンテナンス性が高く、見通しの良い画面構造の実装パターンを紹介します。

画面構造の3層構成

提案する構造は以下の3層で構成されています

  1. NavGraph(画面遷移の管理)
  2. Screen(状態管理)
  3. Content(UI表示)

主なメリット

  • 責務の明確な分離
  • 再利用性の向上
  • テストのしやすさ
  • プレビューの容易さ

各層の役割と実装例

NavGraph層 (Navigation Layer)

画面遷移を管理する最上位層です。

  • 画面遷移の管理
  • 画面間の連携
  • 遷移時のパラメータ受け渡し
@Composable
fun AppNavGraph(
    modifier: Modifier = Modifier,
){
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onMenuClick = { navController.navigate("menu") }
            )
        }

        composable("menu") {
            MenuScreen(
                onBackClick = { navController.popBackStack() }
            )
        }
    }
}

Screen層 (State Manager)

ViewModelとの連携や状態管理を行う層です。

  • ViewModelとの連携
  • 状態管理
  • イベントハンドリング
@Composable
fun HomeScreen(
    onMenuClick: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: HomeViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsState()

    HomeContent(
        onMenuClick = onMenuClick,
        uiState = uiState
    )
}

@Composable
fun MenuScreen(
    onBackClick: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: MenuViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsState()

    MenuContent(
        onBackClick = onBackClick,
        uiState = uiState
    )
}

Content層 (UI Layer)

UI実装に専念する層です。

  • UI表示のみに専念
  • 状態を受け取って表示
  • プレビュー可能な単位
@Composable
fun HomeContent(
    uiState: HomeUiState,
    modifier: Modifier = Modifier,
    onMenuClick: () -> Unit = {},
) {
    Column {
        Text("Home Screen")
        Button(onClick = onMenuClick) {
            Text("Go to Menu")
        }
    }
}

@Preview
@Composable
fun HomeContentPreview() {
    val uiState = HomeUiState()

    HomeContent(
        uiState = uiState,
    )
}

Content層の実装における注意点

プレビューの保守性の観点から避けるべきパターン
// ❌ プレビューが困難なパターン
@Composable
fun HomeContent(
    viewModel: HomeViewModel,
    navController: NavController
) {
    // ViewModelやNavControllerに依存した実装
}
推奨されるパターン
// ✅ プレビューが容易なパターン
@Composable
fun HomeContent(
    uiState: UiState,
    onButtonClick: () -> Unit
) {
    // UI描画に専念した実装
}

@Preview
@Composable
fun HomeContentPreview() {
    HomeContent(
        uiState = UiState(),
        onButtonClick = {}
    )
}

なぜContent層にViewModelやNavControllerを直接渡すべきでないのか

  1. プレビューの困難さ

    • Hiltなどの依存性注入を使用したViewModelやNavControllerを必要とするComposableは@Previewで確認できない
    • UI開発のイテレーションが遅くなる
  2. テストの複雑化

    • ViewModelやNavControllerのモック作成が必要
    • UIのユニットテストが書きにくい
  3. 関心の分離が不十分

    • UIロジックとビジネスロジックが混在
    • コードの再利用性が低下

3層構成パターンのメリット

関心の分離

  • 各層の責務が明確
  • コードの見通しが良い
  • 変更の影響範囲が限定的
  • UIロジックとビジネスロジックの分離

テスタビリティ

  • Content層は簡単にプレビュー可能
    • ViewModelやNavControllerに依存しない設計
    • UIの迅速な開発イテレーション
  • Screen層はViewModelのテストで対応
    • ビジネスロジックの単体テストが容易
    • モックの作成が最小限
  • 画面遷移のテストが容易
    • Navigation層での独立したテストが可能

メンテナンス性

  • 新機能追加時の影響範囲が明確
  • バグの特定が容易
  • チーム開発での役割分担がしやすい
  • コードの再利用性が向上
    • Content層のUIコンポーネントの再利用
    • Screen層のロジック共有

開発効率

  • プレビューによる迅速なUI開発
  • コードレビューの効率化
    • 変更範囲が明確

補足:NavHostについて

NavHostの実装

NavHostは、Jetpack Composeを利用したAndroidアプリの画面遷移を管理するための重要なコンポーネントです。
従来のAndroidアプリ開発では、Activityの切り替えやFragmentの制御で画面遷移を実現していましたが、
Jetpack ComposeではこのNavHostを使用することで、シンプルかつ宣言的に画面遷移を実装できます。

NavHostの基本的な役割

  • アプリ内の画面遷移の制御
  • 画面間のデータの受け渡し
  • バックスタック(戻る操作)の管理

以下のコード例では、HomeScreenとMenuScreenという2つの画面間の遷移を実装しています

@Composable
fun AppNavGraph() {
    val navController = rememberNavController()

    NavHost(
        navController = navController, // 画面遷移を制御するコントローラー
        startDestination = "home" // アプリ起動時に表示する画面
    ) {
        // "home"という名前の画面を定義
        composable("home") {
            HomeScreen(
                onMenuClick = { navController.navigate("menu") } // メニュー画面への遷移
            )
        }

// "menu"という名前の画面を定義 composable("menu") { MenuScreen( onBackClick = { navController.popBackStack() } // 前の画面に戻る ) } } }

コードの解説

  1. rememberNavController()
    • 画面遷移を管理するコントローラーを作成
    • アプリ内での画面遷移の履歴を保持
  2. NavHost
    • 画面遷移の定義をまとめるコンテナ
    • startDestinationで最初に表示する画面を指定
  3. composable()
    • 各画面の定義
    • 第一引数の文字列(例:”home”)が画面のIDとなる
  4. 画面遷移の方法
    • navController.navigate(“menu”)で指定した画面に遷移
    • navController.popBackStack()で前の画面に戻る

このように、NavHostを使用することで、画面遷移のロジックを一箇所にまとめて管理できます。
従来のFragment遷移と比べて、コードがシンプルになり、保守性も向上します。

補足:型安全な画面遷移の実装

画面遷移時のデータは @Serializable アノテーションを使用したモデルで管理することで、型安全な実装が可能です。

@Serializable
sealed class ScreenRoute {
    data object Home: ScreenRoute() // 引数が不要な場合はdata object
    data class Detail( // 引数が必要な場合はdata class
        val id: String,
        val title: String
    ): ScreenRoute()
}

@Composable
fun AppNavGraph(
    navController: NavHostController = rememberNavController()
) {
    NavHost(
        navController = navController,
        startDestination = ScreenRoute.Home::class
    ) {
        composable<ScreenRoute.Home> {
            HomeScreen(
                onNavigateToDetail = { id, title ->
                navController.navigate(ScreenRoute.Detail(id, title))
                }
            )
        }

       composable<ScreenRoute.Detail> { backStackEntry ->
            val route = backStackEntry.toRoute<ScreenRoute.Detail>() // 型安全にパラメータを取得
            DetailScreen(
                id = route.id,
                title = route.title
            )
        }
    }
}

この方法のメリット:

  • 型安全なデータの受け渡しが可能
  • データクラスの利用で意図が明確
  • コンパイル時の型チェック
  • 画面遷移の引数が明確に定義される


※ 
@Serializableを使用するには、kotlinx-serializationの依存関係追加が必要です:

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
}


まとめ

この3層構造を採用することで、Jetpack Composeを使用したアプリ開発において、
より整理された、メンテナンス性の高いコードベースを実現できます。

特に規模の大きいアプリケーションにおいて、この構造は真価を発揮します。
画面数が増えても破綻しにくく、チームでの開発もスムーズに進められます。

参考

Google 公式サンプル

  • Now in Android

    • Googleの最新のベストプラクティスを実装
    • マルチモジュール構成での3層構造の実例
  • Jetcaster

    • シンプルな構成での実装例
    • Content層とScreen層の分離が分かりやすい
  • Jetnews

    • ナビゲーションの実装例
    • 状態管理の参考に最適
  • Navigation での型安全性
    • 公式推奨の実装方法
    • 型安全な引数の受け渡し

これらのサンプルは定期的にアップデートされ、最新のComposeのプラクティスを反映しています。特に「Now in Android」は実際のプロダクションアプリを想定した実装になっており、本記事で説明した構造の実践的な応用例として参考になります。


    Latest最新記事

    Popular人気の記事