モード変更


    言語

Jetpack Composeでナビゲーションパラメータ、ダイナミックなstartDestinationを実現

2023/03/03

私たちのチームは最近、2つの別々のAndroidアプリケーションを必要とするクライアントとのコラボレーションで、Jetpack Composeと全く新しいプロジェクトを行う機会を得ました。このプロジェクトは無事リリースされ、クライアントは最終製品に非常に満足していました。 このブログでは、Jetpack Compose ナビゲーションのパラメータの扱いについて、特に最初はかなり厄介だったので、その過程で得たいくつかの学びを共有したいと思います。

執筆時点では、Jetpack Compose Navigationの最新バージョンは 2.5.3 でしたが, しかし、私たちは2.5.0を使用しました。

この記事に記載するすべてのコードは、カスタムのビジネスロジックを削除し、必要最低限に絞り込み、教育目的にのみ使用されています。

アプリのデザイン

ロジックの流れ

ログイン画面では、ログインに成功した後、データベースに初回ログインかどうかのフラグがあるかどうかをチェックする。初回ログインの場合は、「パスワード変更画面」に遷移し、パスワードを変更する。それ以外の場合は、「ホーム画面」に送られる。

Login Flow Design

ただし、パスワードの変更画面は、ホーム画面からもいつでも呼び出すことができます。

Home Screen to Change Password

パスワードの変更画面で、もし firstLogin が false ならば、ユーザーはまず現在のパスワードを入力するよう求められます。true の場合、現在のパスワードの入力は必要ありません。

主な課題 : ダイナミックな startDestination を実現する。

Googleは、startDestinationを:

ランチャーからアプリを起動したときに、ユーザーが最初に見る画面です。また、「戻る」ボタンを押した後にランチャーに戻ったときにも、この画面が表示されます。

そしてさらに、次のように明記しています :

作るすべてのアプリは、固定されたスタート地点 を持っています。

しかし、私たちのアプリの設計では:

パスワード変更画面でAndroidの戻るボタンが押された場合。

  • firstLogin == trueの場合、アプリを終了します。
  • firstLogin == falseの場合、通常通り(ホーム画面へ)戻る。

したがって、startDestinationを変更する方法を見つけなければなりません。

Jetpack Composeのナビゲーション

プロジェクトの基本レイアウト

Jetpack Composeプロジェクトの基本的な構造(パラメータなし)は、次のようなものです:

@Composable
fun MainScreen(
    mainViewModel: MainViewModel = viewModel()
) {
    val navController = rememberNavController()

    Scaffold {
        val startDestination = mainViewModel.startDestination.collectAsState().value

        NavHost(
            navController = navController,
            startDestination = startDestination
        ) {
            composable(AppRoute.LOGIN.route) {
                LoginScreen(
                    navController = navController
                )
            }
            composable(AppRoute.HOME.route) {
                HomeScreen(
                    navController = navController
                )
            }
            composable(AppRoute.CHANGE_PASSWORD.route) {
                ChangePasswordScreen(
                    navController = navController
                )
            }
        }
    }
}

@Composable
fun LoginScreen(navController: NavController) { ... }

@Composable
fun HomeScreen(navController: NavController) { ... }

@Composable
fun ChangePasswordScreen(navController: NavController) { ... }

この文脈では、MainViewModelは以下のようになります:

class MainViewModel(application: Application) : AndroidViewModel(application) {
    private val _startDestination = MutableStateFlow(AppRoute.LOGIN.route)
    val startDestination: StateFlow<String> get() = _startDestination

    private fun updateStartDestination(value: String) {
        _startDestination.value = value
    }
}

また、AppRoute enumは以下のように定義されています:

enum class AppRoute(
    val route: String
) {
    LOGIN("login"),
    HOME("home"),
    CHANGE_PASSWORD("change_password")
}

この設定では、デフォルトの startDestination は AppRoute.LOGIN.route に設定されているので、アプリが起動しログイン画面が表示されることになります。

パラメータの追加

将来的に複数のパラメータを追加したい場合は、すべてのパラメータをデータクラスに格納することをお勧めします。データクラスは、GSONライブラリを使って、JSONとの変換や解析が簡単にできるのが便利です。

data class ChangePasswordScreenArguments(
    val isFirstLogin: Boolean
)

JSONのシリアライズ/デシリアライズを抽象化するための便利なJSON/String拡張ファイル:

object ExtensionJSON {
    fun ChangePasswordScreenArguments.toJson(): String =
        URLEncoder.encode(Gson().toJson(this), StandardCharsets.UTF_8.toString())

    fun String.toChangePasswordScreenArguments(): ChangePasswordScreenArguments =
        Gson().fromJson(this, ChangePasswordScreenArguments::class.java)
}

パスワード変更画面NavHostの構成定義に、初回ログインを表すパラメータを追加する必要があるようになります:

NavHost(/**/) {
	/**/
	composable("${AppRoute.CHANGE_PASSWORD.route}/{${AppRouteParameter.CHANGE_PASSWORD_SCREEN_ARGS.value}}") { backStackEntry ->
        val changePasswordScreenJson =
                        backStackEntry.arguments?.get(AppRouteParameter.CHANGE_PASSWORD_SCREEN_ARGS.value) as String
        ChangePasswordScreen(
            navController = navController,
            arguments = changePasswordScreenJson.toChangePasswordScreenArguments()
        )
    }			
}

@Composable
fun ChangePasswordScreen(navController: NavController, arguments: ChangePasswordScreenArguments) { /**/ }

このパラメータを追加することで、このパラメータに依存する ChangePasswordScreen のビジネスロジックを実装することができます。しかし、動的な startDestination の問題はまだ解決していません。

パラメータを持つ startDestination

1度目の挑戦:クラッシュ

MainViewModel では、init ブロックに、アプリが起動した後に、現在ログインしているユーザーの firstLogin の値を取得し、それに応じて startDestination を更新するコードを追加します。

class MainViewModel(application: Application) : AndroidViewModel(application) {
    // ユーザーデータを含むFirestoreコレクション用のレポです。このブログとは関係ない内容です。
    private val userRepository: UserRepository =
        getApplication<App>().userRepository

    init {
        viewModelScope.launch {
            if (userRepository.isFirstLogin()) {
                updateStartDestination(
                    "${AppRoute.CHANGE_PASSWORD.route}/{${AppRouteParameter.CHANGE_PASSWORD_SCREEN_ARGS.value}}"
                )
            } else {
                updateStartDestination(AppRoute.HOME.route)
            }
        }
    }
}

現在、firstLogin を false に設定すると、アプリは正常にホーム画面にナビゲートされます。 しかし、firstLogin を true に設定すると、アプリは次のような例外を発生してクラッシュします:

java.lang.IllegalArgumentException: navigation destination change_password/{isFirstLogin:true} is not a direct child of this NavGraph

調べてみると、Jetpack Compose Navigation は Navhost 内のコンポーザブルの定義と startDestination が一致する必要があり、ルートに引数が含まれるとややこしいことになるようです。

2度目の挑戦:成功

解決策は、startDestinationとして使用する引数に defaultValue を指定し、さらにその type を指定する必要があります。今回の場合、引数はJSONで、これはStringです。

composable(
    route = "${AppRoute.CHANGE_PASSWORD.route}/{${AppRouteParameter.CHANGE_PASSWORD_SCREEN_ARGS.value }}",
    arguments = listOf(navArgument(AppRouteParameter.CHANGE_PASSWORD_SCREEN_ARGS.value ) {
        //必要な変更点は以下の2行です。
        type = NavType.StringType; defaultValue = ChangePasswordScreenArguments(
            isFirstLogin = true
        ).toJson()
})
) {
    backStackEntry ->
    val changePasswordScreenJson =
        backStackEntry.arguments?.get(AppRouteParameter.CHANGE_PASSWORD_SCREEN_ARGS.value) as String
    ChangePasswordScreen(
        navController = navController,
        arguments = changePasswordScreenJson.toChangePasswordScreenArguments()
    )
}

Credits

Article Photo by Leue, Holger

Jetpack Compose logo Google Developers

Sextant Image Freepik

androidcomposekotlinnavigation

Author

Diarmaid Lindsay

Diarmaid Lindsay

Android/Flutter Tech Lead

Google & Open Source fan

その他おすすめ記事

2026/05/19

ゴールデンテスト - AI駆動開発における実践的なテストコードを考える

モンスターラボのエンジニアリングマネージャーの奥田です。 AIによりコードが大量に生成されるようになった今、ソフトウェアの品質担保はこれまで以上に重要なテーマとなりました。品質担保のためにテストコードを書くというのは多くの開発現場で行われていますし、テストコードも生成AIが書けるようになったことでテストカバレッジは大幅に向上していると思います。 一方で、大量に生成されたプロダクトコード、テストコードをチェックするのは大変な作業ですし、AIなどのツールを利用するにしてもガードレールとチェックポイントを適切に...

Shuhei Okuda

Shuhei Okuda

Test

2026/05/14

「ビジネスアナリストが仕様を書き、エンジニアが実装する」をAI時代に再設計する ── Spec Kitをオフショア開発にカスタマイズした話(設計編)

本記事では、github/spec-kit(以下、Spec Kit)をオフショア × AI駆動開発のプロジェクトに導入するにあたって、標準のSpec Kitに対してどんな設計判断を重ねてきたかを書き残します。本格運用はこれから始まります。だからこそ、判断のプロセスと、設計時点で見えている懸念を、後から検証可能な形で残しておきたいと考えました。 オフショア × AI駆動開発で感じている摩擦 ある一覧APIの仕様書には、「並び順に従って表示する」とだけ書いてあります。実装したエンジニアは、既存の同様のAPIに...

Daisuke Oba

Daisuke Oba

Architecture

サービス開発実績会社情報
採用情報インサイトお問い合わせ
© 2022 Monstarlab
情報セキュリティ基本方針個人情報の取り扱いについて個人情報保護方針