Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
917a444
Adding tests for Navigator
dturner Jul 21, 2025
e88d470
First draft of Navigator for nested and shared destinations
dturner Jul 30, 2025
1b9681c
Address AI feedback
dturner Jul 31, 2025
8a36488
Add Nav2 activity and dependencies
dturner Jul 16, 2025
2104f89
Add starting point for migration
dturner Jul 31, 2025
6a56eca
Step 1 of migration complete
dturner Jul 31, 2025
24d2fb9
Lots of refactoring
dturner Jul 31, 2025
caed027
Add Step 4 of the migration
dturner Aug 1, 2025
40482df
Add Step 5 of the migration
dturner Aug 12, 2025
0110faa
Part way through step 6 of migration
dturner Aug 13, 2025
309f912
Updating all steps
dturner Aug 14, 2025
5019ac2
Add final step 7.
dturner Aug 14, 2025
3e9bc63
Tidy up imports
dturner Aug 15, 2025
26f754d
Merge branch 'main' into dt/2to3migration
dturner Aug 15, 2025
0d9b38e
Merge branch 'main' into dt/2to3migration
dturner Aug 15, 2025
17b4015
Navigator uses SavedState APIs and KotlinX Serialization to persist s…
dturner Aug 18, 2025
de6bdf4
Remove remaining Nav2 references from step 7
dturner Aug 18, 2025
466b3a1
Handle shared routes
dturner Aug 18, 2025
201f488
Switch to using Saver
dturner Aug 18, 2025
1356254
Add recipes for returning navigation results
Aug 19, 2025
b29d10b
Fix errors on result event sample
Aug 19, 2025
3cc8f30
Clean up result code
jbw0033 Aug 19, 2025
1c25c4c
Refactor result passing with a unified ResultStore
jbw0033 Aug 19, 2025
6bb1fe4
Update headings in README for better structure
dturner Sep 2, 2025
d468a91
Minor updates to result recipe
jbw0033 Sep 3, 2025
ba8221f
Update to latest library versions and disable snapshot artifact repo
dturner Sep 11, 2025
4141b46
Replace SnapshotStateList with NavBackStack
dturner Sep 11, 2025
fd7be63
Address Gemini feedback
dturner Sep 11, 2025
d23149d
Add link to bug
dturner Sep 11, 2025
5928c4d
Merge pull request #79 from android/dt/update-versions
dturner Sep 11, 2025
93f9730
Create Supporting Pane Recipe
tiwiz Sep 11, 2025
efee1c1
Create Supporting Pane Recipe
tiwiz Sep 11, 2025
17a045d
Update dependencies to sync with Supporting Pane PR
tiwiz Sep 12, 2025
fb49912
Merge branch 'dependencies-update' into supporting-pane
tiwiz Sep 12, 2025
28146a5
Merge pull request #81 from android/dependencies-update
ianhanniballake Sep 14, 2025
c324bab
Fix comments
tiwiz Sep 18, 2025
f178728
Merge remote-tracking branch 'origin/main' into supporting-pane
tiwiz Sep 18, 2025
2f164b9
Fix comments
tiwiz Sep 18, 2025
f496dbc
Update README file
tiwiz Sep 18, 2025
d4db6e1
Fix naming
tiwiz Sep 18, 2025
975d88e
Merge pull request #80 from android/supporting-pane
ianhanniballake Sep 24, 2025
c054009
Add koin injected ViewModel recipe
dturner Sep 25, 2025
3f5fe09
Slight formatting change
dturner Sep 25, 2025
a8e577e
Addressing AI code review
dturner Sep 25, 2025
dccb089
Merge pull request #83 from android/dt/koin
ianhanniballake Sep 27, 2025
bce2b1b
Move Material recipes to their specific package
tiwiz Sep 29, 2025
47b4df3
Update README.md
tiwiz Sep 29, 2025
1f0c2ee
Update README.md
dturner Sep 29, 2025
496be6a
Update README.md
dturner Sep 29, 2025
8def1d6
Update README.md
dturner Sep 29, 2025
804f572
Update README.md
dturner Sep 29, 2025
a0841fe
Merge pull request #85 from android/material
tiwiz Sep 29, 2025
b83fe0d
Provide a rememberResultStore Composable
jbw0033 Oct 2, 2025
a857897
Update to alpha10
dturner Oct 2, 2025
2648ce8
Update gradle/libs.versions.toml
dturner Oct 2, 2025
72108df
Add migration guide
dturner Oct 3, 2025
0c9eb4b
Update guide
dturner Oct 3, 2025
7510ef3
Update guide
dturner Oct 3, 2025
a8da9c4
Update guide
dturner Oct 3, 2025
90f66ab
Update guide
dturner Oct 3, 2025
52032b2
Update guide
dturner Oct 3, 2025
f31ea9e
Update guide
dturner Oct 3, 2025
f46b3fc
Update guide
dturner Oct 3, 2025
5c94195
Merge branch 'main' into dt/2to3migration
dturner Oct 3, 2025
26133a4
Remove navigator package
dturner Oct 3, 2025
ab087e7
Merge remote-tracking branch 'origin/results' into results
jbw0033 Oct 3, 2025
aaab38f
Merge pull request #89 from android/dt/alpha10
jbw0033 Oct 3, 2025
64f615e
Merge remote-tracking branch 'origin/results' into results
jbw0033 Oct 3, 2025
4beb9c0
Refactor to render NavDisplay on top of NavHost, rather than wrapping it
dturner Oct 3, 2025
598c263
Merge branch 'main' into dt/2to3migration
dturner Oct 3, 2025
beeeddc
Update to use alpha10 API
dturner Oct 3, 2025
00e4e2a
Merge pull request #46 from android/dt/2to3migration
dturner Oct 3, 2025
78dd22b
Update links in migration guide
dturner Oct 3, 2025
08b63ff
Fix link to migration package
dturner Oct 3, 2025
28947aa
Fix version
dturner Oct 3, 2025
fbbe7e8
Merge remote-tracking branch 'origin/results' into results
jbw0033 Oct 3, 2025
d17bfdc
Apply suggestion from @Olabraaten
ianhanniballake Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".results.event.ResultEventActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".results.state.ResultStateActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Nav3Recipes"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import com.example.nav3recipes.dialog.DialogActivity
import com.example.nav3recipes.modular.hilt.ModularActivity
import com.example.nav3recipes.passingarguments.basicviewmodels.BasicViewModelsActivity
import com.example.nav3recipes.passingarguments.injectedviewmodels.InjectedViewModelsActivity
import com.example.nav3recipes.results.event.ResultEventActivity
import com.example.nav3recipes.results.state.ResultStateActivity
import com.example.nav3recipes.scenes.materiallistdetail.MaterialListDetailActivity
import com.example.nav3recipes.scenes.twopane.TwoPaneActivity
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
Expand Down Expand Up @@ -71,6 +73,10 @@ private val recipes = listOf(
Heading("Passing navigation arguments"),
Recipe("Argument passing to basic ViewModel", BasicViewModelsActivity::class.java),
Recipe("Argument passing to injected ViewModel", InjectedViewModelsActivity::class.java),

Heading("Returning Results"),
Recipe("Return result as Event", ResultEventActivity::class.java),
Recipe("Return result as State", ResultStateActivity::class.java),
)

class RecipePickerActivity : ComponentActivity() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.nav3recipes.results.event

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect

/**
* An Effect to provide a result even between different screens
*
* The trailing lambda provides the result from a flow of results.
*
* @param resultEventBus the ResultEventBus to retrieve the result from
* @param resultKey the key that should be associated with this effect
* @param onResult the callback to invoke when a result is received
*/
@Composable
inline fun <reified T> ResultEffect(
resultEventBus: ResultEventBus = LocalResultEventBus.current,
resultKey: String = T::class.toString(),
crossinline onResult: suspend (T) -> Unit
) {
LaunchedEffect(resultKey, resultEventBus.channelMap[resultKey]) {
resultEventBus.getResultFlow<T>()?.collect { result ->
onResult.invoke(result as T)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.nav3recipes.results.event

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import kotlinx.serialization.Serializable

/**
* This recipe demonstrates passing an event result to a previous screen. It does this by:
*
* - Providing a [ResultEventBus]
* - Implementing a [ResultEffect] in the receiving screen
* - Calling [ResultEventBus.sendResult] from the sending screen.
*/


@Serializable
data object Home : NavKey

@Serializable
class ResultPage : NavKey

class ResultEventActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
val resultBus = remember { ResultEventBus() }
CompositionLocalProvider(LocalResultEventBus.provides(resultBus)) {
Scaffold { paddingValues ->

val backStack = rememberNavBackStack(Home)

NavDisplay(
backStack = backStack,
modifier = Modifier.padding(paddingValues),
onBack = { backStack.removeLastOrNull() },
entryProvider = { key ->
when (key) {
is Home -> NavEntry(key) {
val viewModel = viewModel<HomeViewModel>(key = Home.toString())
ResultEffect<Name> { name ->
viewModel.name = name
}

Column {
Text("Welcome to Nav3")
Button(onClick = {
backStack.add(ResultPage())
}) {
Text("Click to provide a name")
}
if (viewModel.name == null) {
Text("I don't know who you are")
} else {
Text("Hi, ${viewModel.name}")
}
}
}

is ResultPage -> NavEntry(key) {
Column {

val state = rememberTextFieldState()
OutlinedTextField(
state = state,
label = { Text("Please enter a name") }
)
Button(onClick = {
resultBus.sendResult<Name>(result = state.text.toString())
backStack.removeLastOrNull()
}) {
Text("Return name")
}
}
}
else -> NavEntry(key) { Text("Unknown route") }
}
}
)
}
}
}
}
}

class HomeViewModel : ViewModel() {
var name by mutableStateOf<String?>(null)
}

typealias Name = String
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.example.nav3recipes.results.event

import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.compositionLocalOf
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
import kotlinx.coroutines.flow.receiveAsFlow

/**
* Local for receiving results in a [ResultEventBus]
*/
object LocalResultEventBus {
private val LocalResultEventBus: ProvidableCompositionLocal<ResultEventBus?> =
compositionLocalOf { null }

/**
* The current [ResultEventBus]
*/
val current: ResultEventBus
@Composable
get() = LocalResultEventBus.current ?: error("No ResultStore has been provided")

/**
* Provides a [ResultEventBus] to the composition
*/
infix fun provides(
bus: ResultEventBus
): ProvidedValue<ResultEventBus?> {
return LocalResultEventBus.provides(bus)
}
}
/**
* An EventBus for passing results between multiple sets of screens.
*
* It provides a solution for event based results.
*/
class ResultEventBus {
/**
* Map from the result key to a channel of results.
*/
val channelMap: MutableMap<String, Channel<Any?>> = mutableMapOf()

/**
* Provides a flow for the given resultKey.
*/
inline fun <reified T> getResultFlow(resultKey: String = T::class.toString()) =
channelMap[resultKey]?.receiveAsFlow()

/**
* Sends a result into the channel associated with the given resultKey.
*/
inline fun <reified T> sendResult(resultKey: String = T::class.toString(), result: T) {
if (!channelMap.contains(resultKey)) {
channelMap.put(resultKey, Channel(capacity = BUFFERED, onBufferOverflow = BufferOverflow.SUSPEND))
}
channelMap[resultKey]?.trySend(result)
}

/**
* Removes all results associated with the given key from the store.
*/
inline fun <reified T> removeResult(resultKey: String = T::class.toString()) {
channelMap.remove(resultKey)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.nav3recipes.results.state

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import kotlinx.serialization.Serializable

/**
* This recipe demonstrates passing an state result to a previous screen. It does this by:
*
* - Providing a [ResultStore]
* - Calling [ResultStore.getResultState] in the receiving screen
* - Calling [ResultStore.setResult] from the sending screen.
*/

@Serializable
data object Home : NavKey

@Serializable
class ResultPage : NavKey

class ResultStateActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
val resultStore = remember { ResultStore() }
CompositionLocalProvider(LocalResultStore.provides(resultStore)) {
Scaffold { paddingValues ->
val backStack = rememberNavBackStack(Home)

NavDisplay(
backStack = backStack,
modifier = Modifier.padding(paddingValues),
onBack = { backStack.removeLastOrNull() },
entryProvider = { key ->
when (key) {
is Home -> NavEntry(key) {
val name = resultStore.getResultState<Name?>()

Column {
Text("Welcome to Nav3")
Button(onClick = {
backStack.add(ResultPage())
}) {
Text("Click to provide a name")
}
if (name == null) {
Text("I don't know who you are")
} else {
Text("Hi, $name")
}
}


}
is ResultPage -> NavEntry(key) {
Column {

val state = rememberTextFieldState()
OutlinedTextField(
state = state,
label = { Text("Please enter a name") }
)

Button(onClick = {
resultStore.setResult<Name>(result = state.text.toString())
backStack.removeLastOrNull()
}) {
Text("Return name")
}
}
}
else -> NavEntry(key) { Text("Unknown route") }
}
}
)
}
}
}
}
}

typealias Name = String
Loading