Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 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
9caf04f
Add bottom sheet recipe
jbw0033 Aug 19, 2025
bd08782
Remove parenthesis from BottomSheetSceneStrategy
jbw0033 Aug 19, 2025
bde7f74
Minor edits
dturner Aug 21, 2025
6bb1fe4
Update headings in README for better structure
dturner Sep 2, 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
0e5efe3
Add bottom sheet recipe
jbw0033 Aug 19, 2025
2bb993e
Remove parenthesis from BottomSheetSceneStrategy
jbw0033 Aug 19, 2025
8d0e30d
Minor edits
dturner Aug 21, 2025
f512dfe
Merge remote-tracking branch 'origin/bottomsheet' into bottomsheet
jbw0033 Oct 2, 2025
df2d43e
Add bottom sheet recipe
jbw0033 Aug 19, 2025
c303cd0
Remove parenthesis from BottomSheetSceneStrategy
jbw0033 Aug 19, 2025
49cff71
Minor edits
dturner Aug 21, 2025
2115ada
Merge remote-tracking branch 'origin/bottomsheet' into bottomsheet
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
aaab38f
Merge pull request #89 from android/dt/alpha10
jbw0033 Oct 3, 2025
4ea983d
Merge remote-tracking branch 'origin/bottomsheet' into bottomsheet
jbw0033 Oct 3, 2025
495c65b
Fix errors in Bottomsheet Recipe
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
13642c3
Merge remote-tracking branch 'origin/bottomsheet' into bottomsheet
jbw0033 Oct 3, 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
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,34 @@ use its APIs to implement common navigation use cases.
## Recipes
These are the recipes and what they demonstrate.

**Basic API examples**
### Basic API examples
- **[Basic](app/src/main/java/com/example/nav3recipes/basic)**: Shows most basic API usage.
- **[Saveable back stack](app/src/main/java/com/example/nav3recipes/basicsaveable)**: As above, with a persistent back stack.
- **[Entry provider DSL](app/src/main/java/com/example/nav3recipes/basicdsl)**: As above, using the entryProvider DSL.

**Layouts and animations**
- **[Material adaptive](app/src/main/java/com/example/nav3recipes/scenes/materiallistdetail)**: Shows how to use a Material list-detail layout.
### Layouts and animations
- **[Dialog](app/src/main/java/com/example/nav3recipes/dialog)**: Shows how to create a Dialog destination.
- **[Custom Scene](app/src/main/java/com/example/nav3recipes/scenes/twopane)**: Shows how to create a custom layout using a `Scene` and `SceneStrategy` (see video of UI behavior below).
- **[Animations](app/src/main/java/com/example/nav3recipes/animations)**: Override the default animations for all destinations and a single destination.

**Common use cases**
### Material adaptive layouts
Examples showing how to use the layouts provided by the [Compose Material3 Adaptive Navigation3 library](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive#compose_material3_adaptive_navigation3_version_10_2)
- **[List-Detail](app/src/main/java/com/example/nav3recipes/material/listdetail)**: Shows how to use a Material adaptive list-detail layout.
- **[Supporting Pane](app/src/main/java/com/example/nav3recipes/material/supportingpane)**: Shows how to use a Material adaptive supporting pane layout.

### Common use cases
- **[Common navigation UI](app/src/main/java/com/example/nav3recipes/commonui)**: A common navigation toolbar where each item in the toolbar navigates to a top level destination.
- **[Conditional navigation](app/src/main/java/com/example/nav3recipes/conditional)**: Switch to a different navigation flow when a condition is met. For example, for authentication or first-time user onboarding.

**Architecture**
### Architecture
- **[Modularized navigation code](app/src/main/java/com/example/nav3recipes/modular/hilt)**: Demonstrates how to decouple navigation code into separate modules (uses Dagger/Hilt for DI).

**Passing navigation arguments to ViewModels**
- **[Basic ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/basicviewmodels)**: Navigation arguments are passed to a ViewModel constructed using `viewModel()`
- **[Hilt injected ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/injectedviewmodels)**: Navigation arguments are passed to a ViewModel constructed using `hiltViewModel()`
### Passing navigation arguments to ViewModels
- **[Basic ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/basic)**: Navigation arguments are passed to a ViewModel constructed using `viewModel()`
- **[Hilt injected ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/hilt)**: Navigation arguments are passed to a ViewModel constructed using `hiltViewModel()`
- **[Koin injected ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/koin)**: Navigation arguments are passed to a ViewModel constructed using `koinViewModel()`

**Planned**
### Planned
- **Deeplinks**: Create and handle deeplinks to specific destinations
- **Android XR**: Custom navigation and layout behavior for Android XR
- **Returning a result from a destination**: Return a result to a previous destination
Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ dependencies {
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.navigation2)

implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.koin.compose.viewmodel)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
Expand All @@ -91,4 +93,5 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
testImplementation(kotlin("test"))
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.example.nav3recipes

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isSelectable
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso
import com.example.nav3recipes.migration.start.StartMigrationActivity
import com.example.nav3recipes.migration.step2.Step2MigrationActivity
import com.example.nav3recipes.migration.step3.Step3MigrationActivity
import com.example.nav3recipes.migration.step4.Step4MigrationActivity
import com.example.nav3recipes.migration.step5.Step5MigrationActivity
import com.example.nav3recipes.migration.step6.Step6MigrationActivity
import com.example.nav3recipes.migration.step7.Step7MigrationActivity
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters

/**
* Instrumented navigation tests for each of the migration steps.
*/
@RunWith(Parameterized::class)
class MigrationActivityNavigationTest(activityClass: Class<out ComponentActivity>) {

@get:Rule(order = 0)
val composeTestRule = createAndroidComposeRule(activityClass)

companion object {
@JvmStatic
@Parameters(name = "{0}")
fun data(): Collection<Array<Any>> {
return listOf(
arrayOf(StartMigrationActivity::class.java),
arrayOf(Step2MigrationActivity::class.java),
arrayOf(Step3MigrationActivity::class.java),
arrayOf(Step4MigrationActivity::class.java),
arrayOf(Step5MigrationActivity::class.java),
arrayOf(Step6MigrationActivity::class.java),
arrayOf(Step7MigrationActivity::class.java)
)
}
}

@Test
fun firstScreen_isA() {
composeTestRule.apply {
onNode(hasText("Route A") and isSelectable()).assertIsSelected()
onNodeWithText("Route A title").assertExists()
}
}

@Test
fun navigateToB_selectsB() {
composeTestRule.apply {
onNode(hasText("Route B") and isSelectable()).performClick()
onNode(hasText("Route B") and isSelectable()).assertIsSelected()
onNodeWithText("Route B title").assertExists()
}
}

@Test
fun navigateToA1_keepsASelected() {
composeTestRule.apply {
onNode(hasText("Route A") and isSelectable()).assertIsSelected()
onNodeWithText("Route A title").assertExists()
onNodeWithText("Go to A1").performClick()
onNodeWithText("Route A1 title").assertExists()
onNode(hasText("Route A") and isSelectable()).assertIsSelected()
}
}

@Test
fun navigateAtoBtoC_selectsCAndShowsContent() {
composeTestRule.apply {
onNode(hasText("Route B") and isSelectable()).performClick()
onNode(hasText("Route B") and isSelectable()).assertIsSelected()
onNodeWithText("Route B title").assertExists()

onNode(hasText("Route C") and isSelectable()).performClick()
onNode(hasText("Route C") and isSelectable()).assertIsSelected()
onNodeWithText("Route C title").assertExists()
}
}

@Test
fun navigateAtoB_pressBack_showsA() {
composeTestRule.apply {
onNode(hasText("Route B") and isSelectable()).performClick()
onNode(hasText("Route B") and isSelectable()).assertIsSelected()
onNodeWithText("Route B title").assertExists()

Espresso.pressBack()

onNode(hasText("Route A") and isSelectable()).assertIsSelected()
onNodeWithText("Route A title").assertExists()
}
}

@Test
fun navigateAtoA1_pressBack_showsAContent() {
composeTestRule.apply {
onNodeWithText("Go to A1").performClick()
onNodeWithText("Route A1 title").assertExists()
onNode(hasText("Route A") and isSelectable()).assertIsSelected()

Espresso.pressBack()

onNodeWithText("Route A title").assertExists()
onNode(hasText("Route A") and isSelectable()).assertIsSelected()
}
}

@Test
fun navigateAtoBtoC_thenBack_showsA() {
composeTestRule.apply {
onNode(hasText("Route B") and isSelectable()).performClick()
onNode(hasText("Route B") and isSelectable()).assertIsSelected()
onNodeWithText("Route B title").assertExists()

onNode(hasText("Route C") and isSelectable()).performClick()
onNode(hasText("Route C") and isSelectable()).assertIsSelected()
onNodeWithText("Route C title").assertExists()

Espresso.pressBack()

onNode(hasText("Route A") and isSelectable()).assertIsSelected()
onNodeWithText("Route A title").assertExists()
onNodeWithText("Route B title").assertDoesNotExist()
}
}

/**
* TODO: Investigate why these dialog tests sometimes fail.
*/
@Test
fun navigateToDialogD_onA_showsDialogContentAndDismisses() {
composeTestRule.apply {

onNodeWithText("Open dialog D").performClick()
onNodeWithText("Route D title (dialog)").assertExists()
Espresso.pressBack()
onNodeWithText("Route A title").assertExists()
}
}

@Test
fun navigateToDialogD_onB_showsDialogContentAndDismisses() {
composeTestRule.apply {

onNode(hasText("Route B") and isSelectable()).performClick()

onNodeWithText("Open dialog D").performClick()
onNodeWithText("Route D title (dialog)").assertExists()
Espresso.pressBack()
onNodeWithText("Route B title").assertExists()
}
}


@Test
fun navigateToDialogD_onC_showsDialogContentAndDismisses() {
composeTestRule.apply {

onNode(hasText("Route C") and isSelectable()).performClick()

onNodeWithText("Open dialog D").performClick()
onNodeWithText("Route D title (dialog)").assertExists()
Espresso.pressBack()
onNodeWithText("Route C title").assertExists()
}
}
}
47 changes: 44 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".scenes.materiallistdetail.MaterialListDetailActivity"
android:name=".bottomsheet.BottomSheetActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".material.listdetail.MaterialListDetailActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".material.supportingpane.MaterialSupportingPaneActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
Expand All @@ -82,18 +90,51 @@
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".passingarguments.basicviewmodels.BasicViewModelsActivity"
android:name=".passingarguments.viewmodels.basic.BasicViewModelsActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".passingarguments.injectedviewmodels.InjectedViewModelsActivity"
android:name=".passingarguments.viewmodels.hilt.HiltViewModelsActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".modular.hilt.ModularActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".passingarguments.viewmodels.koin.KoinViewModelsActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".migration.start.StartMigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".migration.step2.Step2MigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".migration.step3.Step3MigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".migration.step4.Step4MigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".migration.step5.Step5MigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".migration.step6.Step6MigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".migration.step7.Step7MigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ import com.example.nav3recipes.animations.AnimatedActivity
import com.example.nav3recipes.basic.BasicActivity
import com.example.nav3recipes.basicdsl.BasicDslActivity
import com.example.nav3recipes.basicsaveable.BasicSaveableActivity
import com.example.nav3recipes.bottomsheet.BottomSheetActivity
import com.example.nav3recipes.commonui.CommonUiActivity
import com.example.nav3recipes.conditional.ConditionalActivity
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.scenes.materiallistdetail.MaterialListDetailActivity
import com.example.nav3recipes.passingarguments.viewmodels.basic.BasicViewModelsActivity
import com.example.nav3recipes.passingarguments.viewmodels.hilt.HiltViewModelsActivity
import com.example.nav3recipes.passingarguments.viewmodels.koin.KoinViewModelsActivity
import com.example.nav3recipes.material.listdetail.MaterialListDetailActivity
import com.example.nav3recipes.material.supportingpane.MaterialSupportingPaneActivity
import com.example.nav3recipes.scenes.twopane.TwoPaneActivity
import com.example.nav3recipes.ui.setEdgeToEdgeConfig

Expand All @@ -56,8 +59,11 @@ private val recipes = listOf(
Recipe("Basic Saveable", BasicSaveableActivity::class.java),

Heading("Layouts and animations"),
Recipe("Bottom Sheet", BottomSheetActivity::class.java),
Recipe("Material list-detail layout", MaterialListDetailActivity::class.java),
Recipe("Material supporting-pane layout", MaterialSupportingPaneActivity::class.java),
Recipe("Dialog", DialogActivity::class.java),
Recipe("Material list-detail layout", MaterialListDetailActivity::class.java),
Recipe("Two pane layout (custom scene)", TwoPaneActivity::class.java),
Recipe("Animations", AnimatedActivity::class.java),

Expand All @@ -68,9 +74,10 @@ private val recipes = listOf(
Heading("Architecture"),
Recipe("Modular Navigation", ModularActivity::class.java),

Heading("Passing navigation arguments"),
Recipe("Argument passing to basic ViewModel", BasicViewModelsActivity::class.java),
Recipe("Argument passing to injected ViewModel", InjectedViewModelsActivity::class.java),
Heading("Passing navigation arguments using ViewModels"),
Recipe("Basic", BasicViewModelsActivity::class.java),
Recipe("Using Hilt", HiltViewModelsActivity::class.java),
Recipe("Using Koin", KoinViewModelsActivity::class.java),
)

class RecipePickerActivity : ComponentActivity() {
Expand Down
Loading