Skip to content

Commit 1356254

Browse files
Jeremy Woodsjbw0033
authored andcommitted
Add recipes for returning navigation results
This commit adds two new recipes demonstrating how to return a result from one screen to a previous one. The "Return result as Event" recipe uses a `ResultStore` backed by Kotlin `Channel`s. A screen can send a result, and the previous screen can listen for it using a `ResultEffect` composable. The "Return result as State" recipe uses a `rememberResult` composable, which wraps `mutableStateOf`. The state is hoisted and can be updated by a child screen.
1 parent d048dec commit 1356254

File tree

7 files changed

+301
-0
lines changed

7 files changed

+301
-0
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@
9494
android:exported="true"
9595
android:label="@string/app_name"
9696
android:theme="@style/Theme.Nav3Recipes"/>
97+
<activity
98+
android:name=".results.event.ResultEventActivity"
99+
android:exported="true"
100+
android:label="@string/app_name"
101+
android:theme="@style/Theme.Nav3Recipes"/>
102+
<activity
103+
android:name=".results.state.ResultStateActivity"
104+
android:exported="true"
105+
android:label="@string/app_name"
106+
android:theme="@style/Theme.Nav3Recipes"/>
97107
</application>
98108

99109
</manifest>

app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import com.example.nav3recipes.dialog.DialogActivity
3535
import com.example.nav3recipes.modular.hilt.ModularActivity
3636
import com.example.nav3recipes.passingarguments.basicviewmodels.BasicViewModelsActivity
3737
import com.example.nav3recipes.passingarguments.injectedviewmodels.InjectedViewModelsActivity
38+
import com.example.nav3recipes.results.event.ResultEventActivity
39+
import com.example.nav3recipes.results.state.ResultStateActivity
3840
import com.example.nav3recipes.scenes.materiallistdetail.MaterialListDetailActivity
3941
import com.example.nav3recipes.scenes.twopane.TwoPaneActivity
4042
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
@@ -71,6 +73,10 @@ private val recipes = listOf(
7173
Heading("Passing navigation arguments"),
7274
Recipe("Argument passing to basic ViewModel", BasicViewModelsActivity::class.java),
7375
Recipe("Argument passing to injected ViewModel", InjectedViewModelsActivity::class.java),
76+
77+
Heading("Returning Results"),
78+
Recipe("Return result as Event", ResultEventActivity::class.java),
79+
Recipe("Return result as State", ResultStateActivity::class.java),
7480
)
7581

7682
class RecipePickerActivity : ComponentActivity() {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.example.nav3recipes.results.event
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
6+
@Composable
7+
inline fun <reified T> ResultEffect(
8+
resultStore: ResultStore = LocalResultStore.current,
9+
resultKey: String = T::class.toString(),
10+
crossinline onResult: suspend (T) -> Unit
11+
) {
12+
LaunchedEffect(resultKey, resultStore.channelMap[resultKey]) {
13+
resultStore.getResultFlow<T>()?.collect { result ->
14+
onResult.invoke(result as T)
15+
}
16+
}
17+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.nav3recipes.results.event
18+
19+
import android.os.Bundle
20+
import androidx.activity.ComponentActivity
21+
import androidx.activity.compose.setContent
22+
import androidx.compose.foundation.layout.Column
23+
import androidx.compose.foundation.layout.padding
24+
import androidx.compose.foundation.text.input.rememberTextFieldState
25+
import androidx.compose.material3.Button
26+
import androidx.compose.material3.OutlinedTextField
27+
import androidx.compose.material3.Scaffold
28+
import androidx.compose.material3.Text
29+
import androidx.compose.ui.Modifier
30+
import androidx.lifecycle.ViewModel
31+
import androidx.lifecycle.viewmodel.compose.viewModel
32+
import androidx.navigation3.runtime.NavEntry
33+
import androidx.navigation3.runtime.NavKey
34+
import androidx.navigation3.runtime.rememberNavBackStack
35+
import androidx.navigation3.ui.NavDisplay
36+
import kotlinx.serialization.Serializable
37+
38+
@Serializable
39+
data object Home : NavKey
40+
41+
@Serializable
42+
class ResultPage : NavKey
43+
44+
class ResultEventActivity : ComponentActivity() {
45+
46+
override fun onCreate(savedInstanceState: Bundle?) {
47+
super.onCreate(savedInstanceState)
48+
49+
setContent {
50+
val resultStore = LocalResultStore.current
51+
52+
Scaffold { paddingValues ->
53+
54+
val backStack = rememberNavBackStack(Home)
55+
56+
NavDisplay(
57+
backStack = backStack,
58+
modifier = Modifier.padding(paddingValues),
59+
onBack = { backStack.removeLastOrNull() },
60+
entryProvider = { key ->
61+
when (key) {
62+
is Home -> NavEntry(key) {
63+
val viewModel = viewModel<HomeViewModel>(key = Home.toString())
64+
ResultEffect<Name?>(resultStore) { name ->
65+
viewModel.name = name
66+
}
67+
68+
Column {
69+
Text("Welcome to Nav3")
70+
Button(onClick = {
71+
backStack.add(ResultPage())
72+
}) {
73+
Text("Click to navigate")
74+
}
75+
Text("My returned name is ${viewModel.name}")
76+
}
77+
78+
79+
}
80+
is ResultPage -> NavEntry(key) {
81+
Column {
82+
83+
val state = rememberTextFieldState()
84+
OutlinedTextField(
85+
state = state,
86+
label = { Text("Result to Return") }
87+
)
88+
Button(onClick = {
89+
resultStore.sendResult<Name>(result = state.text as String)
90+
backStack.removeLastOrNull()
91+
}) {
92+
Text("Return result")
93+
}
94+
}
95+
}
96+
else -> NavEntry(key) { Text("Unknown route") }
97+
}
98+
}
99+
)
100+
}
101+
}
102+
}
103+
}
104+
105+
class HomeViewModel : ViewModel() {
106+
var name: String? = null
107+
}
108+
109+
typealias Name = String
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.example.nav3recipes.results.event
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.ProvidableCompositionLocal
5+
import androidx.compose.runtime.ProvidedValue
6+
import androidx.compose.runtime.compositionLocalOf
7+
import kotlinx.coroutines.channels.BufferOverflow
8+
import kotlinx.coroutines.channels.Channel
9+
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
10+
import kotlinx.coroutines.flow.receiveAsFlow
11+
12+
13+
object LocalResultStore {
14+
private val LocalResultStore: ProvidableCompositionLocal<ResultStore?>
15+
get() {
16+
return compositionLocalOf { null }
17+
}
18+
19+
val current: ResultStore
20+
@Composable
21+
get() = LocalResultStore.current ?: ResultStore()
22+
23+
infix fun provides(
24+
store: ResultStore
25+
): ProvidedValue<ResultStore?> {
26+
return LocalResultStore.provides(store)
27+
}
28+
}
29+
30+
class ResultStore {
31+
val channelMap: MutableMap<String, Channel<Any?>> = mutableMapOf()
32+
33+
inline fun <reified T> getResultFlow(resultKey: String = T::class.toString()) =
34+
channelMap[resultKey]?.receiveAsFlow()
35+
36+
inline fun <reified T> sendResult(resultKey: String = T::class.toString(), result: T) {
37+
if (!channelMap.contains(resultKey)) {
38+
channelMap.put(resultKey, Channel(capacity = BUFFERED, onBufferOverflow = BufferOverflow.SUSPEND))
39+
}
40+
channelMap[resultKey]?.trySend(result)
41+
}
42+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.nav3recipes.results.state
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.remember
6+
7+
@Composable
8+
fun <T> rememberResult(defaultValue: T) =
9+
remember { mutableStateOf(defaultValue) }.apply { value = defaultValue }
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.nav3recipes.results.state
18+
19+
import android.os.Bundle
20+
import androidx.activity.ComponentActivity
21+
import androidx.activity.compose.setContent
22+
import androidx.compose.foundation.layout.Column
23+
import androidx.compose.foundation.layout.padding
24+
import androidx.compose.foundation.text.input.rememberTextFieldState
25+
import androidx.compose.material3.Button
26+
import androidx.compose.material3.OutlinedTextField
27+
import androidx.compose.material3.Scaffold
28+
import androidx.compose.material3.Text
29+
import androidx.compose.runtime.getValue
30+
import androidx.compose.runtime.setValue
31+
import androidx.compose.ui.Modifier
32+
import androidx.lifecycle.ViewModel
33+
import androidx.lifecycle.viewmodel.compose.viewModel
34+
import androidx.navigation3.runtime.NavEntry
35+
import androidx.navigation3.runtime.NavKey
36+
import androidx.navigation3.runtime.rememberNavBackStack
37+
import androidx.navigation3.ui.NavDisplay
38+
import kotlinx.serialization.Serializable
39+
40+
@Serializable
41+
data object Home : NavKey
42+
43+
@Serializable
44+
class ResultPage : NavKey
45+
46+
class ResultStateActivity : ComponentActivity() {
47+
48+
override fun onCreate(savedInstanceState: Bundle?) {
49+
super.onCreate(savedInstanceState)
50+
51+
setContent {
52+
Scaffold { paddingValues ->
53+
var result by rememberResult<Name?>(defaultValue = null)
54+
val backStack = rememberNavBackStack(Home)
55+
56+
NavDisplay(
57+
backStack = backStack,
58+
modifier = Modifier.padding(paddingValues),
59+
onBack = { backStack.removeLastOrNull() },
60+
entryProvider = { key ->
61+
when (key) {
62+
is Home -> NavEntry(key) {
63+
val viewModel = viewModel<HomeViewModel>(key = Home.toString())
64+
viewModel.name = result
65+
66+
Column {
67+
Text("Welcome to Nav3")
68+
Button(onClick = {
69+
backStack.add(ResultPage())
70+
}) {
71+
Text("Click to navigate")
72+
}
73+
Text("My returned name is ${viewModel.name}")
74+
}
75+
76+
77+
}
78+
is ResultPage -> NavEntry(key) {
79+
Column {
80+
81+
val state = rememberTextFieldState()
82+
OutlinedTextField(
83+
state = state,
84+
label = { Text("Result to Return") }
85+
)
86+
87+
Button(onClick = {
88+
result = state.text as String
89+
backStack.removeLastOrNull()
90+
}) {
91+
Text("Return result")
92+
}
93+
}
94+
}
95+
else -> NavEntry(key) { Text("Unknown route") }
96+
}
97+
}
98+
)
99+
}
100+
}
101+
}
102+
}
103+
104+
class HomeViewModel : ViewModel() {
105+
var name: String? = null
106+
}
107+
108+
typealias Name = String

0 commit comments

Comments
 (0)