Navigation3の背景と問題
Jetpack Navigation3はAndroid Jetpack ComposeまたはCompose MultiplatformでStackベースの画面遷移を実現するAndroid Jetpackライブラリです。
Navigation3はStack構造によるデータドリブンのナビゲーションであり、Compose設計と相性が良く、マルチモジュールプロジェクト構成においてfeature module間で互いに依存することなく画面遷移が実現可能となります。
Navigation3の設計は素晴らしいのですが、画面間で任意の結果を受け渡す仕組みは提供されていません。
結果を受け渡す仕組みが存在しなければ、遷移元の画面が遷移先の画面に依存してしまいfeature moduleを疎結合に保つことができません。
Compose Navigation3 ResultState
Compose Navigation3 ResultStateライブラリ(以降ResultStateと表記)を導入することで、Navigation3の画面間疎結合を保ったまま画面間で結果を受け渡すことができます。
ResultStateは以下の機能を提供します。
- Navigation3での画面の結果の受け渡し
- 結果は
State<NavigationResult?>
として提供され、Composableから更新を監視できる - 結果をSavedStateへ保持し、Activity再生成やプロセスの再起動でも永続化・復帰できる
- 任意の画面から結果を返すことができ、それを任意の画面から受け取ることができる
- 結果を返す画面と受け取る画面がStack内で離れていても受け取ることができる
- 受け取り側の画面がStackからpopされると、保持していた結果は自動的に破棄される
-
@Serializable
data classを受け渡すことができる
この記事ではNavigation3の現在の状況を簡単に説明し、ResultStateの使い方を説明します。
Jetpack Compose版Navigation3とCompose Multiplatform版Navigation3について
現時点(2025/09/17)ではNavigation 3は1.0.0-alpha09がリリースされています。
Navigation 3はCompose Multiplatform版も提供されています。
androidx.navigation3:navigation3-runtime
はAndroid Jetpack Compose版がKMPライブラリとして提供されており、Compose Multiplatformからそのまま使うことができます。
androidx.navigation3:navigation3-ui
はAndroid Jetpack Compose専用であり、これは別途Compose Multiplatformで利用可能なorg.jetbrains.androidx.navigation3:navigation3-ui
が提供されています。JetBrains Compose Multiplatform版navigation-uiはKMPに対応しているandroidx.navigation3:navigation3-runtime
に依存しています。
JetBrains Compose Multiplatform版のnavigation3-uiはAndroid Jetpack版のnavigation3-uiよりもあとに追いかけてリリースされますが、中身のコードはほぼ同じもの(Android Jetpack版のForkリポジトリ)であり、Android Jetpack版と同じクラス構成で同じ機能を使うことができます。
JetBrains Compose Multiplatform版navigation3-uiは現時点(2025/09/17)で、開発中の1.0.0+dev2962が利用可能です。
JetBrains Compose Multiplatform版を使えば、Android、iOS、macOS、Linux、Windows、Webのすべての環境でNavigation3を利用可能となります。
Compose Navigation3 ResultStateの対応環境
ResultStateはJetpack ComposeとCompose Multiplatformのどちらも対応しています。
Compose Navigation3 ResultStateの使い方 (Androidアプリ向け)
ここでは通常のAndroidアプリへのResultState導入方法を説明します。
Compose Multiplatform向けの説明はcompose-navigation3-resultstateのREADME.mdを参照してください。
依存関係の追加
Androidプロジェクトのbuild.gradle.ktsに依存関係を追加します。
ResultStateのバージョンはcompose-navigation3-resultstateから最新のバージョンを確認し、指定してください。
// build.gradle.kts
plugins {
id("com.android.application")
// ...
}
dependencies {
// ReusltStateの追加
implementation("io.github.irgaly.navigation3.resultstate:resultstate:{version}")
// 以降、navigation3-uiの依存など
implementation("androidx.navigation3:navigation3-ui:...")
// ...
}
NavDisplayにNavigationResultNavEntryDecoratorを設定する
ResultStateは画面の結果をStringのKeyで特定し、結果をStringとして受け渡せるようになっています。すべてをStringで保持することによりSavedStateを使った永続化と復帰に対応しています。
しかし、実際には画面間の結果はdata classを受け渡しできると便利です。
ResultStateはKotlinx Serializationの@Serializable
data classの受け渡しに対応しています。今回は@Serializable
受け渡しの実装で説明します。
NavBackStackの要素として以下のScreen1、Screen2があるとします。
@Serializable
sealed interface Screen : NavKey
@Serializable
object Screen1 : Screen
@Serializable
object Screen2 : Screen
Screen2からScreen1へ結果を返すこととします。結果は以下のdata classとします。
@Serializable
data class Screen2Result(val result: String)
最初に、Serializableに対応したKeyを定義します。
// "Screen2Result"をresultKeyとし、Screen2Resultを返す定義
val Screen2ResultKey = SerializableNavigationResultKey<Screen2Result>(
serializer = Screen2Result.serializer(),
resultKey = "Screen2Result",
)
次に、NavDisplayは以下のように実装します。
Screen1で結果を受け取るためScreen1に対応するNavEntryのmetadataへNavigationResultMetadata.resultConsumer()を設定し、NavDisplayへrememberNavigationResultNavEntryDecorator()を渡します。
@Composable
fun NavigationContent() {
val navBackStack = rememberNavBackStack(Screen1)
val entryProvider = entryProvider<Screen> {
entry<Screen1>(
metadata = NavigationResultMetadata.resultConsumer(
// 受け取りたいSerializableNavigationResultKeyを指定する
Screen2ResultKey,
)
) {
Screen1(...)
}
entry<Screen2> {
Screen2(...)
}
}
NavDisplay(
backStack = navBackStack,
onBack = { ... },
entryDecorators = listOf(
rememberSceneSetupNavEntryDecorator(),
// rememberNavigationResultNavEntryDecoratorに、
// NavDisplayへ渡したものと同一のentryProviderを渡す
rememberNavigationResultNavEntryDecorator(
navBackStack = navBackStack,
entryProvider = entryProvider,
),
rememberSavedStateNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = entryProvider,
)
}
Screen1で結果を受け取る
Screen1では、NavigationResultConsumerからState<SerializedNavigationResult<Screen2Result>?>
を受け取りその変化を監視します。
Screen2から結果が返されるとState<SerializedNavigationResult<Screen2Result>?>
から結果を取り出すことができます。
結果を処理したあとは、以降のCompositionで同じ値を受け取ることのないように結果を削除します。
@Composable
fun Screen1(...) {
val json: Json = Json
val resultConsumer = LocalNavigationResultConsumer.current
var resultString: String by rememberSaveable { mutableStateOf("{empty}") }
val screen2Result: SerializedNavigationResult<Screen2Result>? by remember(resultConsumer) {
// Screen2ResultKeyを渡し、型安全にStateを取り出す
resultConsumer.getResultState(json, Screen2ResultKey)
}
LaunchedEffect(screen2Result) {
val result: SerializedNavigationResult<Screen2Result>? = screen2Result
if (result != null) {
// getResult()により結果をデシリアライズしてScreen2Resultを取り出す
val screen2Result: Screen2Result = result.getResult()
// 結果を処理したらclearResult()で結果を削除する
resultConsumer.clearResult(result.resultKey)
}
}
Column {
Text("Screen1")
Text("Received result is: $resultString")
}
}
Screen2から結果を返す
Screen2では、NavigationResultProducer経由で結果を返します。
setResult()によりScreen2ResultがStringへエンコードされてSavedStateにより保持されます。
@Composable
fun Screen2(...) {
val json: Json = Json
val resultProducer = LocalNavigationResultProducer.current
Column {
Text("Screen2")
Button(onClick = {
resultProducer.setResult(
json,
Screen2ResultKey,
Screen2Result("my result of screen2!"),
)
}) {
Text("Set a result to Screen2ResultKey")
}
}
}
これにより画面間で結果を受け渡すことができるようになりました。
次の手順を踏むことでScreen2の結果をScreen1で受け取ることができます。
- Screen1からScreen2へ遷移する
- Screen2でScreen2ResultをsetResult()する
- Screen2からScreen1へ画面を戻る
- Screen1のCompositionが走り、screen2ResultからScreen2Resultを受け取る
その他
ResultStateの基本的な使い方を説明しました。
動作原理など詳しいことはcompose-navigation3-resultstateのREADME.mdを参照してください。