11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[KMP]Navigation Composeで型安全&ネスト構造の画面遷移を実装してみた

Last updated at Posted at 2025-06-12

はじめに

4月からKotlin Multiplatformを用いた開発に取り組んでいる者です。

Jetpack Composeでアプリ開発を進めていくと、避けて通れないのがナビゲーションの設計。
この記事では私たちのチームが公式ドキュメントを参考にしつつ、議論の末導き出したナビゲーションの設計についてご紹介したいと思います。

参考にした公式ドキュメント

目指すナビゲーション構成

例として、語学学習アプリを作成するとして考えてみます。

以下のような画面構成を想定してみましょう。

- Login
- Main (Bottom Nav)
   ├── Library
   │     ├── Search
   │     └── Lesson(lessonIdを引数に受け取る)
   └── Account
         ├── Settings
         └── AboutApp

ログイン後にメインの画面に遷移し、ボトムナビゲーションによってレッスン一覧(Library)とアカウント情報画面(Account)を切り替えられるようになっています。
また、LibraryとAccountそれぞれの画面から別の画面への遷移も持っています。

これをCompose Navigationでどう表現するか?が今回のテーマです。

SerializableなRoute定義で型安全なナビゲーション

まず、各画面のルートを@Serializableなオブジェクトやデータクラスで定義します。

@Serializable
object Login

@Serializable
object Main {

    @Serializable
    object LibraryGraph {
        @Serializable
        object Library
        
        @Serializable
        object Search
        
        @Serializable
        data class Lesson(val lessonId: Int)
    }

    @Serializable
    object AccountGraph {
        @Serializable
        object Account
        
        @Serializable
        object Settings
        
        @Serializable
        object AboutApp
    }
}

オブジェクトのネスト構造によってナビゲーション全体の構造も視覚的に把握しやすくなっています。
データクラスを使用することで、引数の受け渡しも型安全に行えます。

NavHostの分割とネスト構造

ルートのNavHostを2つに分けて実装しています。

  • AppNavHost: ログイン → メイン画面への遷移を管理
  • MainNavHost: BottomNavigationによるのLibrary / Account画面遷移を管理
@Composable
fun AppNavHost(navController: NavHostController, startDestination: Any) {
    NavHost(
        navController = navController,
        startDestination = startDestination,
    ) {
        loginGraph(navController)
    }
}

fun NavGraphBuilder.loginGraph(
    navController: NavHostController,
) {
    composable<Login> {
        LoginScreen(
            navigateToMain = {
                navController.navigate(Main) {
                    popUpTo(Login) { inclusive = true }
                }
            },
        )
    }
    composable<Main> {
        val mainNavController = rememberNavController()
        MainScreen(
            navigateToLibrary = { mainNavController.navigate(Main.LibraryGraph) },
            navigateToAccount = { mainNavController.navigate(Main.AccountGraph) },
        )
    }
}

MainNavHostではlibraryGraphaccountGraphをネストしており、それぞれのナビゲーション責務を分離しています。

@Composable
fun MainNavHost(
    appState: DirectBooksAppState,
    modifier: Modifier,
) {
    val mainNavController = appState.mainNavController
    NavHost(
        navController = mainNavController,
        startDestination = Main.LibraryGraph,
        modifier = modifier,
    ) {
        libraryGraph(navController = mainNavController)
        accountGraph(
            navController = mainNavController,
        )
    }
}

LibraryGraphの定義

fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
    navigation<Main.LibraryGraph>(startDestination = Main.LibraryGraph.Library) {
        composable<Main.LibraryGraph.Library> {
            LibraryScreen(
                navigateToSearch = { navController.navigate(Main.LibraryGraph.Search) },
                navigateToLesson = { lessonId ->
                    navController.navigate(Main.LibraryGraph.Lesson(lessonId))
                }
            )
        }
        composable<Main.LibraryGraph.Search> {
            SearchScreen(navigateToBack = { navController.popBackStack() })
        }
    }

    lessonGraph(navController)
}

lessonGraph は Lesson 画面への遷移をまとめた別関数として定義しています(本記事では割愛)。
また、Lesson画面はdata classとして定義しているので、引数を安全に渡せます。

AccountGraphの定義

こちらも同様にネストされた構造で記述しています。

fun NavGraphBuilder.accountGraph(navController: NavHostController) {
    navigation<Main.AccountGraph>(startDestination = Main.AccountGraph.Account) {
        composable<Main.AccountGraph.Account> {
            AccountScreen(
                navigateToSettings = {
                    navController.navigate(Main.AccountGraph.Settings)
                },
                navigateToAboutApp = {
                    navController.navigate(Main.AccountGraph.AboutApp)
                }
            )
        }
        composable<Main.AccountGraph.Settings> {
            SettingsScreen(navigateToBack = { navController.popBackStack() })
        }
        composable<Main.AccountGraph.AboutApp> {
            AboutAppScreen(navigateToBack = { navController.popBackStack() })
        }
    }
}

まとめ

  • ナビゲーションのネスト構造をオブジェクトで自然に表現できる
  • navigation<T>composable<T>でネスト構造もシンプルに実装できる
  • AppNavHostMainNavHost、およびlibraryGraphaccountGraphの分割により責務が明確になる
  • 引数付き画面(例:Lesson)もデータクラスで安全に遷移可能

ナビゲーションの実装に試行錯誤を繰り返しましたが、直感的にわかりやすい設計になったのではないかと思います。

ぜひご自身のプロジェクトでも試してみてください!

11
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?