Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d1e7fd0
First commit for List-Detail with variable number of columns
tiwiz Sep 5, 2025
d019a52
Started strategy for Three Panes List-List-Detail
tiwiz Sep 5, 2025
a420ca1
Initial behaviour - TODO: select multiple items
tiwiz Sep 5, 2025
3d174d9
Initial behaviour - TODO: select multiple items
tiwiz Sep 5, 2025
356b779
Select multiple items, but only show one in the backstack
tiwiz Sep 5, 2025
a18d4ea
Add possibility to show third panel with the second. Crashing when go…
tiwiz Sep 5, 2025
462e181
Merge branch 'main' into list-detail-no-placeholder
tiwiz Oct 2, 2025
d8af65f
Make List-Detail without placeholders
tiwiz Oct 2, 2025
8e7e3a6
Add README entry
tiwiz Oct 2, 2025
6ac3c4b
Fix PR comments
tiwiz Oct 2, 2025
15b79ec
Introduce support for 3 panes
tiwiz Oct 2, 2025
6fc115f
Introduce support for variable weights
tiwiz Oct 2, 2025
288048d
Introduce support for variable weights
tiwiz Oct 2, 2025
5fa0c86
Code cleanup
tiwiz Oct 3, 2025
0412b5d
Remove single pane scene to leverage default single pane
tiwiz Oct 3, 2025
c07d62e
Move helper function closer to its usage
tiwiz Oct 3, 2025
ebc76bc
Add some more comments
tiwiz Oct 3, 2025
c7785d2
Add possibility to also show SupportingPane
tiwiz Oct 6, 2025
9893103
Fix build issues
tiwiz Oct 6, 2025
29d23c3
Merge branch 'main' into list-detail-no-placeholder
tiwiz Oct 6, 2025
9906549
Fix build issues
tiwiz Oct 6, 2025
3c0967b
Add more customization for BottomSheet
tiwiz Oct 6, 2025
ed9c748
Adds logic to link the new items in the root activity, and moves Mate…
tiwiz Oct 8, 2025
1dd297d
Fix Toolbar navigation
tiwiz Oct 8, 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
6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ These are the recipes and what they demonstrate.
- **[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.
- **[List-Detail without placeholder](app/src/main/java/com/example/nav3recipes/scenes/listdetailnoplaceholder)**: Shows how to make a list-detail without a placeholder, adapting the number of columns to the window size

### 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)
Expand Down
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ dependencies {
implementation(libs.androidx.material3.windowsizeclass)
implementation(libs.androidx.adaptive.layout)
implementation(libs.androidx.material3.navigation3)
implementation(libs.androidx.window)
implementation(libs.androidx.window.core)


implementation(libs.kotlinx.serialization.core)
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
android:name=".material.supportingpane.MaterialSupportingPaneActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".scenes.listdetailnoplaceholder.ListDetailNoPlaceholderActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".scenes.twopane.TwoPaneActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.example.nav3recipes.passingarguments.viewmodels.hilt.HiltViewModelsAc
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.listdetailnoplaceholder.ListDetailNoPlaceholderActivity
import com.example.nav3recipes.scenes.twopane.TwoPaneActivity
import com.example.nav3recipes.ui.setEdgeToEdgeConfig

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

Heading("Layouts and animations"),
Recipe("Material list-detail layout", MaterialListDetailActivity::class.java),
Recipe("Material supporting-pane layout", MaterialSupportingPaneActivity::class.java),
Recipe("Dialog", DialogActivity::class.java),
Recipe("Two pane layout (custom scene)", TwoPaneActivity::class.java),
Recipe("Animations", AnimatedActivity::class.java),
Recipe("List-Detail and Supporting Pane", ListDetailNoPlaceholderActivity::class.java),

Heading("Material"),
Recipe("Material list-detail layout", MaterialListDetailActivity::class.java),
Recipe("Material supporting-pane layout", MaterialSupportingPaneActivity::class.java),

Heading("Common use cases"),
Recipe("Common UI", CommonUiActivity::class.java),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/*
* 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.
*/

@file:OptIn(ExperimentalMaterial3Api::class)
package com.example.nav3recipes.scenes.listdetailnoplaceholder

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.scene.Scene
import androidx.navigation3.scene.SceneStrategy
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_LARGE_LOWER_BOUND
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND

internal class AdaptiveThreePaneScene<T : Any>(
val firstPane: NavEntry<T>,
val secondPane: NavEntry<T>,
val thirdPane: NavEntry<T>,
val weights: ListDetailNoPlaceholderSceneStrategy.SceneDefaults,
override val previousEntries: List<NavEntry<T>>,
override val key: Any
) : Scene<T> {

override val entries: List<NavEntry<T>> = listOf(firstPane, secondPane, thirdPane)

override val content: @Composable (() -> Unit) = {

Row(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.weight(weights.threePanesSceneFirstPaneWeight)) {
firstPane.Content()
}
Column(modifier = Modifier.weight(weights.threePanesSceneSecondPaneWeight)) {
secondPane.Content()
}
Column(modifier = Modifier.weight(weights.threePanesSceneThirdPaneWeight)) {
thirdPane.Content()
}
}
}
}

internal class AdaptiveTwoPaneScene<T : Any>(
val firstPane: NavEntry<T>,
val secondPane: NavEntry<T>,
val weights: ListDetailNoPlaceholderSceneStrategy.SceneDefaults,
override val previousEntries: List<NavEntry<T>>,
override val key: Any
) : Scene<T> {

override val entries: List<NavEntry<T>> = listOf(firstPane, secondPane)

override val content: @Composable (() -> Unit) = {

Row(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.weight(weights.twoPanesScenePaneWeight)) {
firstPane.Content()
}
Column(modifier = Modifier.weight(1 - weights.twoPanesScenePaneWeight)) {
secondPane.Content()
}
}
}
}


internal class BottomPaneScene<T : Any>(
val pane: NavEntry<T>,
val properties: ModalBottomSheetProperties = ModalBottomSheetProperties(),
override val previousEntries: List<NavEntry<T>>,
override val key: Any,
val onBack: (Int) -> Unit
) : Scene<T> {

override val entries: List<NavEntry<T>> = listOf(pane)

@OptIn(ExperimentalMaterial3Api::class)
override val content: @Composable (() -> Unit) = {

ModalBottomSheet(
onDismissRequest = { onBack(1) },
properties = properties
) {
pane.Content()
}

}
}

class ListDetailNoPlaceholderSceneStrategy<T : Any>(val sceneDefaults: SceneDefaults = SceneDefaults()) :
SceneStrategy<T> {

companion object {
internal const val MAIN = "main"
internal const val DETAIL = "detail"
internal const val SUPPORT = "support"
internal const val THIRD_PANEL = "thirdPanel"

@JvmStatic
fun main() = mapOf(MAIN to true)

@JvmStatic
fun detail() = mapOf(DETAIL to true)

@JvmStatic
fun thirdPanel() = mapOf(THIRD_PANEL to true)

@JvmStatic
fun support() = mapOf(SUPPORT to true)
}

data class SceneDefaults(
val twoPanesScenePaneWeight: Float = .5f,
val threePanesSceneFirstPaneWeight: Float = .4f,
val threePanesSceneSecondPaneWeight: Float = .3f,
val threePanesSceneThirdPaneWeight: Float = .3f,
val bottomSheetProperties: ModalBottomSheetProperties = ModalBottomSheetProperties()
)

@Composable
override fun calculateScene(
entries: List<NavEntry<T>>, onBack: (Int) -> Unit
): Scene<T>? {

val windowSizeClass =
currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true).windowSizeClass
val isLastEntrySupportingPane = entries.lastOrNull()?.metadata[SUPPORT] == true

// Condition 1: Only return a Scene if the window is sufficiently wide to render two panes,
// or if a supporting pane is detected.
//
// We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp).
if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) {
return if (isLastEntrySupportingPane) {
buildSupportingPaneScene(
pane = entries.last(),
previousEntry = entries[entries.size - 2],
onBack = onBack
)
} else {
null
}
}

if (windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_LARGE_LOWER_BOUND) && entries.size >= 3) {
return buildAdaptiveThreePanesScene(entries)
}

if (entries.size >= 2) {
return buildAdaptiveTwoPanesScene(entries)
}
return null
}

private fun buildAdaptiveThreePanesScene(entries: List<NavEntry<T>>): Scene<T>? {
val lastEntry = entries.last()
val secondLastEntry = entries[entries.size - 2]
val thirdLastEntry = entries[entries.size - 3]

return if (lastEntry.metadata[THIRD_PANEL] == true && secondLastEntry.metadata[DETAIL] == true && thirdLastEntry.metadata[MAIN] == true) {
AdaptiveThreePaneScene(
firstPane = thirdLastEntry,
secondPane = secondLastEntry,
thirdPane = lastEntry,
weights = sceneDefaults,
previousEntries = listOf(thirdLastEntry, secondLastEntry),
key = Triple(
thirdLastEntry.contentKey, secondLastEntry.contentKey, lastEntry.contentKey
)
)
} else {
null
}
}

private fun NavEntry<T>.isMainPane() : Boolean = metadata[MAIN] == true
private fun NavEntry<T>.isSecondPane() : Boolean = metadata[DETAIL] == true || metadata[SUPPORT] == true
private fun NavEntry<T>.isLastPane() : Boolean = metadata[THIRD_PANEL] == true

private fun buildAdaptiveTwoPanesScene(entries: List<NavEntry<T>>): Scene<T>? {
val lastEntry = entries.last()
val secondLastEntry = entries[entries.size - 2]

return if (lastEntry.isSecondPane() && secondLastEntry.isMainPane()) {
buildListDetailScene(secondLastEntry, lastEntry)
} else if (lastEntry.isLastPane() && secondLastEntry.isSecondPane() && entries.size >= 3) {
val zeroethEntry = entries[entries.size - 3]
buildDetailAndThirdPanelScene(secondLastEntry, lastEntry, zeroethEntry)
} else {
null
}
}

private fun buildListDetailScene(firstEntry: NavEntry<T>, secondEntry: NavEntry<T>): Scene<T> {
return AdaptiveTwoPaneScene(
firstPane = firstEntry,
secondPane = secondEntry,
weights = sceneDefaults,
previousEntries = listOf(firstEntry),
key = Pair(firstEntry.contentKey, secondEntry.contentKey)
)
}

private fun buildDetailAndThirdPanelScene(
firstEntry: NavEntry<T>, secondEntry: NavEntry<T>, previousEntry: NavEntry<T>
): Scene<T> {
return AdaptiveTwoPaneScene(
firstPane = firstEntry,
secondPane = secondEntry,
weights = sceneDefaults,
previousEntries = listOf(previousEntry, firstEntry),
key = Pair(firstEntry.contentKey, secondEntry.contentKey)
)
}

private fun buildSupportingPaneScene(
pane: NavEntry<T>,
previousEntry: NavEntry<T>,
onBack: (Int) -> Unit
): Scene<T> {
return BottomPaneScene(
pane = pane,
properties = sceneDefaults.bottomSheetProperties,
previousEntries = listOf(previousEntry),
key = pane.contentKey,
onBack = onBack
)
}
}
Loading