Compose Navigation3 ResultState
ResultState provides the ability to handle screen results for Compose Navigation3.
Compose Navigation3
is a great library for navigating with stack data driven screen management, that encourages you
to achieve your feature modules become more clearly separated and independently.
However, Navigation3 lacks a Screen Result handling API at this time.
ResultState provides a Result API based on SavedState architecture for both Android Jetpack Compose and
Compose Multiplatform.
The result values are stored into SavedState, and survive through Activity recreation or process restarting correctly.
Also the saved results are tied to NavEntry's lifecycle, and cleared automatically when the receiver screen is popped out.
Supporting Platforms
- Supporting all platforms that Navigation3 supports.
- Android
- JVM
- Native iOS, watchOS, tvOS
- Native macOS
- Native Linux
- Native Windows
- JS, Wasm JS
Usage
Setup
Add ResultState dependency to your project.
for Android Jetpack Compose project
build.gradle.kts
plugins {
id("com.android.application")
}
dependencies {
implementation("io.github.irgaly.navigation3.resultstate:resultstate:1.2.0")
implementation("androidx.navigation3:navigation3-ui:...")
}
for Compose Multiplatform project
build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.application")
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose")
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("io.github.irgaly.navigation3.resultstate:resultstate:1.2.0")
implementation("org.jetbrains.androidx.navigation3:navigation3-ui:...")
}
}
}
}
Using ResultState with NavDisplay
ResultState holds the all results as "String", that is for aiming to easily saved on SavedState
architecture.
- The
result key is just a "String".
- The
result value is just a "String".
So you can produce a result as String with a String result key.
To use ResultState, follow this steps:
- Register the result keys to the consumer screen's NavEntry metadata with
NavigationResultMetadata.resultConsumer() function.
- Set
rememberNavigationResultNavEntryDecorator() to NavDisplay's entryDecorators.
- Receive the result as
State<NavigationResult?> in the consumer screen by
LocalNavigationResultConsumer.
Here is an example of an Android Compose project.
Compose Multiplatform project's sample is also available
in sample/app/src/commonMain/kotlin/io/github/irgaly/navigation3/resultstate/sample/App.kt.
Next, receive the result as State<NavigationResult?> in Screen1.
Finally, produce the result from Screen2.
@Composable
fun Screen2(...) {
val resultProducer: NavigationResultProducer = LocalNavigationResultProducer.current
Column {
Text("Screen2")
Button(onClick = {
resultProducer.setResult(
"Screen2Result",
"my result of screen2!",
)
}) {
Text("Set a result to \"Screen2Result\" key")
}
}
}
That's all!
You can receive the Screen2's result "my result of screen2!" from Screen1, when reentered to Screen1
or realtime because of the result is observed by Screen1 as a State.
Using ResultState with typed result keys and Kotlinx Serialization
ResultState supports to handle the typed result keys and the value as any Serializable type.
Serialization support is provided by extension functions.
Here is an example.
Code Examples
There are some more code examples.
Exmaple: The consumer screen receives multiple results
Receiver screen can receive multiple results from multiple producer screens.
Here is an example that assuming:
- Screen1 is a consumer of "Screen2Result" key and "Screen3Result" key.
- Screen2 produces a result of "Screen2Result" key.
- Screen3 produces a result of "Screen3Result" key.
- Using typed result keys and Kotlinx Serialization pattern.
In this situation, if you'd like to wait for both Screen2Result and Screen3Result are produced,
you can observe both states by single LaunchedEffect. This is an usual Compose way.
Architecture
ResultState will store all results in a MutableState<Map<String, Map<String, String>>>,
that is defined in rememberNavigationResultNavEntryDecorator() or
rememberNavigationResultStateHolder() and it is held by NavigationResultStateHolder.
This map contains all values as String, so it can be saved by SavedState.
@Composable
fun <T : Any> rememberNavigationResultNavEntryDecorator(
backStack: List<T>,
entryProvider: (T) -> NavEntry<*>,
contentKeyToString: (Any) -> String = { it.toString() },
savedStateResults: MutableState<Map<String, Map<String, String>>> = rememberSaveable {
mutableStateOf(emptyMap())
},
): NavEntryDecorator<T> {
navigationResultStateHolder = rememberNavigationResultStateHolder(
backStack = backStack,
entryProvider = entryProvider,
contentKeyToString = contentKeyToString,
savedStateResults = savedStateResults,
)
remember(navigationResultStateHolder) {
NavigationResultNavEntryDecorator(navigationResultStateHolder)
}
}
The map has the structure below:
Map<String, Map<String, String>>
- Key: NavEntry contentKey as String
- Value:
Map<String, String>
- Key: a Result Key as String
- Value: a Result as String
So all consumer screens can store the result map on SavedState.
Any screens can receive the result
ResultState provides the result to all screens that registered as a consumer by NavEntry's metadata,
so any multiple screens and any position at NavBackStack can consume the result.
This means:
- Assume that:
- The NavBackStack is [Screen1, Screen2, Screen3].
- Then, they are all possible:
- Screen1 receives Screen2's result.
- Screen1 receives Screen3's result.
- Screen2 receives Screen3's result.
- Screen3 receives Screen3's result.
An example of ResultState lifecycle
The map's contents are associated with NavEntry's lifecycle.
Here is a state's lifecycle example:
- For example, assume that:
- Screen1's NavEntry contentKey is
"screen1".
The scenario is as follows:
sequenceDiagram
participant NavigationResultStateHolder
participant AppNavHost
participant Screen1
participant Screen2
participant Screen3
AppNavHost->>+Screen1: Show
activate Screen1
Screen1->>+Screen2: Navigate
activate Screen2
Screen2->>NavigationResultStateHolder: produce screen2's result
Screen2->>+Screen3: Navigate
activate Screen3
Screen3->>NavigationResultStateHolder: produce screen3's result
Screen3->>Screen2: Back
deactivate Screen3
Screen2->>Screen1: Back
deactivate Screen2
deactivate Screen1
1. Initial state
The initial state of ResultState map is empty:
| Map Key | Map Value |
|---|
| (empty) | (empty) |
2. Navigated to Screen2, then Screen2 produce a result
Screen2 produced a result "result from screen2" for "Screen2Result" key.
The current ResultState map is:
| Map Key | Map Value |
|---|
"screen1" | "Screen2Result" to "result from screen2" |
3. Navigated to Screen3, then Screen3 produces a result
Screen3 produced a result "result from screen3" for "Screen3Result" key.
The current ResultState map is:
4. Navigated back to Screen2
Navigated back to Screen2, and Screen3 was popped out from the NavBackStack.
Screen3 holds no result in the ResultState map, so the map is not changed.
The current ResultState map is:
Then, Screen2 has consumed the "Screen3Result" result, and called
consumer.clearResult("Screen3Result").
So the ResultState map is:
| Map Key | Map Value |
|---|
"screen1" | "Screen2Result" to "result from screen2"
"Screen3Result" to |
5. Navigated back to Screen1
Navigated back to Screen1, and Screen2 was popped out from the NavBackStack.
Screen2 holds no result in the ResultState map, so the map is not changed.
The current ResultState map is:
| Map Key | Map Value |
|---|
"screen1" | "Screen2Result" to "result from screen2"
"Screen3Result" to |
Then, Screen1 has consumed the "Screen2Result" result and "Screen3Result" result, then called
consumer.clearResult("Screen3Result"), consumer.clearResult("Screen3Result").
So the ResultState map is:
| Map Key | Map Value |
|---|
| (empty) | (empty) |
The results are cleared when the consumer screen is popped out
When it is navigated to Screen1 from Screen3 by skipping Screen2 showing, Screen2 can not consume
the
"Screen3Result" result.
The ResultState map is associated with NavEntry's lifecycle, so the results that the Screen2 holds
are cleared automatically.
sequenceDiagram
participant Screen1
participant Screen2
participant Screen3
activate Screen1
Screen1->>+Screen2: Navigate
activate Screen2
Screen2->>+Screen3: Navigate
activate Screen3
Screen3->>Screen1: Back
deactivate Screen3
deactivate Screen2
deactivate Screen1
The Screen3 has showed and produced a result "result from screen3" to "Screen3Result" key.
The current ResultState map is:
Then, it navigated back to Screen1 from Screen3 directly, while the Screen2 was also popped out.
Screen2 did not consume the "Screen3Result" result, but the results for "screen2" are cleared
automatically.
Then current ResultState map is:
| Map Key | Map Value |
|---|
"screen1" | "Screen2Result" to "result from screen2"
"Screen3Result" to |
ResultState supports multi-pane SceneStrategy
ResultState provides the results as observable State, so the produced results are consumed in
realtime while the consumer screen is showing.
For example, Screen1 and Screen2 are both showing in a multi-pane SceneStrategy, and Screen2
produces a result, then Screen1 can consume the result in realtime by
LaunchedEffect(resultState) { ... }.