モード変更


    言語

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

その他おすすめ記事

2022/12/20

モンスターラボ2023年度新卒内定者向けイベントレポート

 こんにちは、モンスターラボで BackEnd の TechLead をしています、国平です。  このブログには、 Docker on Lima 以来の投稿になります。 今回は、モンスターラボの新卒採用における内定者イベントのレポートをしてみたいと思います。普段の技術的な話題から離れて、新卒内定者向けのイベントレポートを通して、会社の雰囲気をお伝えしてみようと思います。  11 月初旬、モンスターラボで内定者向けに実際のプロジェクトに参加したメンバーから、プロジェクトの様子を伝えて、モンスターラボのプロジ...

Kiyotaka Kunihira

Kiyotaka Kunihira

2022/12/08

[NextJS13] Layout RFC発のアップデート機能紹介!

10 月 25 日に第 6 回 Next.js カンファレンスが開催され、Rust ベースのバンドラーであるTurbopack、Server component を実装した app ディレクトリ、既存コンポーネントのアップデート等が紹介されました! Next.js conference 今回の大型アップデートの一つである app ディレクトリは数年前から議論がなされてきたLayout RFCの一つであり、 Layout RFC には app ディレクトリやそれに合わせて今回のアップデートで紹介された機能が...

Go Nakano

Go Nakano

Frontend

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