Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Navigate back with results in Jetpack Compose Navigation

2025年09月19日に「Dev.to」が公開したITニュース「Navigate back with results in Jetpack Compose Navigation」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Jetpack Compose Navigationで画面遷移後、前の画面に結果を渡すシンプルな方法を紹介。従来の共有ViewModel等より、`navigateForResult`と`navigateBackWithResult`ヘルパー関数を使うことで、ライフサイクル対応かつ型安全にデータをやり取りできる。

ITニュース解説

Jetpack Composeでアプリケーションを開発する際、複数の画面を移動しながら、ある画面でユーザーが入力または選択した情報を、その前の画面に伝えたいという状況は頻繁に発生する。例えば、ユーザーが住所選択画面で特定の住所を選び、その選択結果を注文画面に自動的に反映させたい、といったケースだ。これまでの開発では、このような画面間のデータ連携には、複数の画面で共有されるViewModel、アプリケーション全体で唯一のインスタンスであるシングルトンオブジェクト、あるいは画面遷移のルートパスに直接パラメータを含める方法などが一般的に使われてきた。しかし、これらの方法では、コードが複雑化したり、データの整合性を保つのが難しくなったり、意図しないバグの原因となったりする課題があった。

この課題に対し、よりシンプルで堅牢なデータ受け渡しのアプローチが提案されている。それは、「特定の画面に進む → その画面で何らかの結果が得られるのを待つ → 結果を持って前の画面に戻る」という明確なパターンを用いる方法だ。このアプローチは、AndroidのJetpack Compose NavigationとKotlinのコルーチン、ライフサイクル管理機能を活用し、プロジェクトに簡単に組み込める小さなヘルパーコードとして提供される。

このヘルパーは、Jetpack Compose Navigationの中心的なコンポーネントであるNavControllerに、結果を受け渡すための新しい拡張関数を追加する形で機能する。主要な拡張関数は以下の四つだ。 一つ目は、結果を期待して別の画面へ遷移するためのsuspend fun <T> NavController.navigateForResult(route: Any)関数。これは、汎用的な型Tの結果を待つことができる。 二つ目は、シリアライズ(データ形式を変換して保存・転送可能にする処理)可能なデータを扱うためのsuspend inline fun <reified T> NavController.navigateForSerializableResult(route: Any)関数。複雑なオブジェクトの結果を安全にやり取りする際に便利で、内部でJSON形式への変換を利用する。 三つ目は、現在の画面での処理が完了し、結果を前の画面に渡して戻るためのfun <T> NavController.navigateBackWithResult(value: T)関数。 四つ目は、シリアライズ可能な結果を渡して戻るためのinline fun <reified T> NavController.navigateBackWithSerializableResult(value: T)関数。

これらのヘルパー関数がどのように連携して動作するのか、その仕組みをステップバイステップで説明する。 まず、呼び出し元の画面で、navController.navigateForResult(route)を呼び出す。これにより、指定されたroute(遷移先の画面を識別するパス)に画面が切り替わる。このとき、navigateForResult関数は、現在のナビゲーションスタックのエントリ(currentBackStackEntry)を記憶し、このエントリのライフサイクルを監視するためのLifecycleEventObserverを設定する。この設定により、呼び出し元のコルーチンは、結果が返ってくるまで一時的に処理を停止し、待機状態に入る。

次に、遷移先の画面(呼び出し先の画面)で何らかの処理が行われ、結果が確定したとする。その結果を前の画面に伝えたい場合、navController.navigateBackWithResult(value)を呼び出す。この関数は、引数として渡されたvalue(結果のデータ)を、前の画面のエントリに関連付けられたSavedStateHandleに保存する。SavedStateHandleは、画面がバックグラウンドに回ったり、システムの都合で破棄されたりしても、その画面の状態を一時的に保持できる特別なキー・バリュー形式のストレージだ。結果はRESULT_KEYという固定のキーを使って保存される。データが保存された後、navigateUp()が呼び出され、現在の画面がナビゲーションスタックから取り除かれ、前の画面が表示される。

前の画面に戻ってくると、その画面はライフサイクルのON_STARTイベントを迎える。navigateForResultが事前に設定したLifecycleEventObserverは、このON_STARTイベントを検知する。イベントが検知されると、オブザーバーはSavedStateHandleからRESULT_KEYを使って保存されていた結果を取得し、待機していたコルーチンにその結果を渡して再開させる。結果が取得された後、SavedStateHandleからその結果は削除される。これは、同じキーで保存された古い値が誤って再利用されるのを防ぐためだ。また、役目を終えたLifecycleEventObserverも削除され、不要なリソース消費やメモリリークを防ぐ。

この仕組みの大きな利点は、ライフサイクルに完全に準拠していること、データの型安全性が確保されていること、そして非常に少ないコード量で実現できることだ。ActivityResultContractのような複雑な設定や、複数の画面間で状態を共有するためのShared ViewModel、あるいはイベントバスといった高度な通知メカニズムを別途導入する必要がないため、開発者はより直感的でクリーンなコードを書くことができる。

具体的な使用例として、ユーザーが色を選ぶ画面を想定してみよう。HomeScreen(呼び出し元の画面)には、色選択ボタンがある。このボタンが押されると、rememberCoroutineScope().launch { val result: String? = navController.navigateForResult(Routes.COLOR_PICKER); selectedColor = result }のように、コルーチン内でnavigateForResultを呼び出し、ColorPickerScreenへ遷移する。このとき、HomeScreenのコルーチンはColorPickerScreenから結果が返されるまで一時停止する。 ColorPickerScreen(呼び出し先の画面)では、ユーザーが「赤」や「青」といった色のボタンを選ぶと、例えばnavController.navigateBackWithResult(color)のように、選ばれた色を結果として渡して前の画面に戻る。 HomeScreenに戻ると、一時停止していたコルーチンが再開し、navigateForResultから返された「赤」という文字列などの結果を受け取ることができる。そして、その結果を使って画面上のテキストを更新するなどの処理を実行できる。このように、データの流れが明確になり、アプリケーションの状態管理が容易になる。

このヘルパーを利用する際には、いくつかの注意点がある。 まず、navigateForResultから返される結果は常にNullable(T?)であるため、ユーザーが結果を選ばずに画面をキャンセルして戻ったり、navigateUp()で何も設定せずに戻ったりするケースを考慮し、null値が返された場合の処理を適切に実装する必要がある。 次に、シリアライズ機能を使わないnavigateBackWithResultを利用する場合、呼び出し元と呼び出し先で期待するデータの型が一致しないと、実行時にClassCastException(型変換エラー)が発生する可能性がある。そのため、型の一致には細心の注意を払うべきだ。 また、このメカニズムで渡される結果は、アプリケーションのプロセスが終了した場合(例えば、OSがメモリ不足でアプリを強制終了させた場合など)には保持されない。これは、データが永続的な状態として保存されるのではなく、一時的なコールバックとして機能するためだ。 最後に、同じ画面から複数の結果を同時に待機するような状況は避けるべきだ。もしそのようなニーズがある場合は、結果の競合を防ぐために、アクセスをガードする仕組みや、結果をキューイングする独自の仕組みを導入することを検討する必要がある。

このヘルパーは、Jetpack Compose Navigationスタックを介してデータをシンプルかつライフサイクルに対応した方法で受け渡すための、軽量で強力なパターンを提供する。プロジェクトにこのヘルパーファイルを組み込み、前進する際にはnavigateForResult()を、結果を返して戻る際にはnavigateBackWithResult()を呼び出すだけで、複雑な設定なしに効率的なデータ連携が可能となる。これは、開発者がよりクリーンで管理しやすいコードを書く上で非常に役立つアプローチと言える。