Benutzerdefinierte Layouts mithilfe von Szenen erstellen

Mit Navigation 3 wird ein leistungsstarkes und flexibles System zum Verwalten des UI-Ablaufs Ihrer App über Szenen eingeführt. Mit Szenen können Sie hochgradig angepasste Layouts erstellen, sich an unterschiedliche Bildschirmgrößen anpassen und komplexe Mehrfenster-Erlebnisse nahtlos verwalten.

Szenen

In Navigation 3 ist eine Scene die Grundeinheit, die eine oder mehrere NavEntry-Instanzen rendert. Stellen Sie sich eine Scene als einen bestimmten visuellen Zustand oder Abschnitt Ihrer UI vor, der die Anzeige von Inhalten aus Ihrem Back-Stack enthalten und verwalten kann.

Jede Scene Instanz wird eindeutig durch ihren key und die Klasse von der Scene selbst identifiziert. Diese eindeutige Kennung ist wichtig, da sie die Animation auf oberster Ebene steuert, wenn sich die Scene ändert.

Die Scene-Schnittstelle hat die folgenden Eigenschaften:

  • key: Any: Eine eindeutige Kennung für diese bestimmte Scene-Instanz. Dieser Schlüssel sorgt in Kombination mit der Klasse der Scene für Eindeutigkeit, hauptsächlich für Animationszwecke.
  • entries: List<NavEntry<T>>: Dies ist eine Liste von NavEntry-Objekten, für deren Anzeige Scene verantwortlich ist. Wichtig: Wenn dasselbe NavEntry während einer Übergangsanimation in mehreren Scenes angezeigt wird (z.B. bei einer Übergangsanimation für ein gemeinsames Element), werden seine Inhalte nur von der letzten Ziel-Scene gerendert, in der er angezeigt wird.
  • previousEntries: List<NavEntry<T>>: Diese Eigenschaft definiert die NavEntrys, die sich ergeben würden, wenn in der aktuellen Scene eine „Zurück“-Aktion ausgeführt wird. Sie ist wichtig, um den richtigen Status der intelligenten „Zurück“-Geste zu berechnen, damit NavDisplay den richtigen vorherigen Status vorhersagen und zu ihm übergehen kann. Das kann eine Szene mit einer anderen Klasse und/oder einem anderen Schlüssel sein.
  • content: @Composable () -> Unit: Dies ist die zusammensetzbare Funktion, mit der Sie definieren, wie die Scene ihre entries und alle umgebenden UI Elemente rendert, die für diese Scene spezifisch sind.
  • metadata: Map<String, Any>: Stellt anderen Bibliothekskomponenten wie NavDisplay szenenspezifische Informationen zur Verfügung. Standardmäßig wird die metadata des letzten NavEntry in entries zurückgegeben.

Szenenstrategien

Ein SceneStrategy ist der Mechanismus, der bestimmt, wie eine bestimmte Liste von NavEntrys aus dem Back-Stack angeordnet und in eine Scene überführt werden soll. Wenn eine SceneStrategy die aktuellen Back-Stack-Einträge erhält, stellt sie sich im Wesentlichen zwei Fragen:

  1. Kann ich aus diesen Einträgen eine Scene erstellen? Wenn die SceneStrategy feststellt, dass sie die angegebenen NavEntrys verarbeiten und eine sinnvolle Scene erstellen kann (z.B. ein Dialogfeld oder ein Layout mit mehreren Bereichen), wird fortgefahren. Andernfalls wird null zurückgegeben, sodass andere Strategien die Möglichkeit haben, eine Scene zu erstellen.
  2. Wenn ja, wie soll ich diese Einträge in der Scene? anordnen? Sobald eine SceneStrategy die Einträge verarbeitet, ist sie für die Erstellung einer Scene und die Definition der Anzeige der angegebenen NavEntrys in dieser Scene verantwortlich.

Das Herzstück einer SceneStrategy ist die Methode calculateScene:

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

Diese Methode ist eine Erweiterungsfunktion für einen SceneStrategyScope, die die aktuelle List<NavEntry<T>> aus dem Back-Stack verwendet. Sie sollte eine Scene<T> zurückgeben, wenn sie aus den bereitgestellten Einträgen erfolgreich eine erstellen kann, oder null wenn das nicht möglich ist.

Der SceneStrategyScope ist für die Verwaltung aller optionalen Argumente verantwortlich, die die SceneStrategy möglicherweise benötigt, z. B. ein onBack-Callback.

Zusammenwirken von Szenen und Szenenstrategien

Die NavDisplay ist die zentrale zusammensetzbare Funktion, die Ihren Back-Stack beobachtet und eine oder mehrere SceneStrategys verwendet, um die entsprechende Scene zu bestimmen und zu rendern.

Der NavDisplay's sceneStrategies Parameter erwartet eine Liste von SceneStrategy Instanzen, die für die Berechnung der anzuzeigenden Scene verantwortlich sind. Wenn von den bereitgestellten Strategien keine Scene berechnet wird, greift NavDisplay automatisch standardmäßig auf eine SinglePaneSceneStrategy zurück.

Hier ist eine Aufschlüsselung der Interaktion:

  • Wenn Sie Ihrem Back-Stack Schlüssel hinzufügen oder daraus entfernen (z.B. mit backStack.add() oder backStack.removeLastOrNull()), werden diese Änderungen von NavDisplay beobachtet.
  • Der NavDisplay übergibt die aktuelle Liste der NavEntrys (abgeleitet von den Back Stack-Schlüsseln) in der Reihenfolge an die konfigurierten sceneStrategies und ruft für jede calculateScene auf, bis eine Scene zurückgegeben wird.
  • Wenn ein SceneStrategy erfolgreich ein Scene zurückgibt, rendert NavDisplay dann den content dieses Scene. NavDisplay verwaltet auch Animationen und die intelligente „Zurück“-Geste basierend auf den Eigenschaften der Scene.

Beispiel: Layout mit einem Bereich (Standardverhalten)

Das einfachste benutzerdefinierte Layout ist eine Anzeige mit einem Bereich. Das ist das Standardverhalten, wenn keine andere SceneStrategy Vorrang hat.

data class SinglePaneScene<T : Any>(
    override val key: Any,
    val entry: NavEntry<T>,
    override val previousEntries: List<NavEntry<T>>,
) : Scene<T> {
    override val entries: List<NavEntry<T>> = listOf(entry)
    override val content: @Composable () -> Unit = { entry.Content() }
}

/**
 * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the
 * list.
 */
public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> {
    override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? =
        SinglePaneScene(
            key = entries.last().contentKey,
            entry = entries.last(),
            previousEntries = entries.dropLast(1)
        )
}

Beispiel: Einfaches Listen-Detailansicht-Layout (benutzerdefinierte Scene und Strategie)

In diesem Beispiel wird gezeigt, wie Sie ein einfaches Listen-Detailansicht-Layout erstellen, das unter zwei Bedingungen aktiviert wird:

  1. Die Fensterbreite ist ausreichend, um zwei Bereiche zu unterstützen (d.h. mindestens WIDTH_DP_MEDIUM_LOWER_BOUND).
  2. Der Back-Stack enthält Einträge, die mit bestimmten Metadaten ihre Unterstützung für die Anzeige in einem Listen-Detailansicht-Layout deklariert haben.

Das folgende Snippet ist der Quellcode für ListDetailScene.kt und es enthält sowohl ListDetailScene als auch ListDetailSceneStrategy:

// --- ListDetailScene ---
/**
 * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split.
 *
 */
class ListDetailScene<T : Any>(
    override val key: Any,
    override val previousEntries: List<NavEntry<T>>,
    val listEntry: NavEntry<T>,
    val detailEntry: NavEntry<T>,
) : Scene<T> {
    override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry)
    override val content: @Composable (() -> Unit) = {
        Row(modifier = Modifier.fillMaxSize()) {
            Column(modifier = Modifier.weight(0.4f)) {
                listEntry.Content()
            }
            Column(modifier = Modifier.weight(0.6f)) {
                detailEntry.Content()
            }
        }
    }
}

@Composable
fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass

    return remember(windowSizeClass) {
        ListDetailSceneStrategy(windowSizeClass)
    }
}

// --- ListDetailSceneStrategy ---
/**
 * A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item
 * is the backstack is a detail, and before it, at any point in the backstack is a list.
 */
class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> {

    override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? {

        if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) {
            return null
        }

        val detailEntry =
            entries.lastOrNull()?.takeIf { it.metadata.contains(DetailKey) } ?: return null
        val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: return null

        // We use the list's contentKey to uniquely identify the scene.
        // This allows the detail panes to be displayed instantly through recomposition, rather than
        // having NavDisplay animate the whole scene out when the selected detail item changes.
        val sceneKey = listEntry.contentKey

        return ListDetailScene(
            key = sceneKey,
            previousEntries = entries.dropLast(1),
            listEntry = listEntry,
            detailEntry = detailEntry
        )
    }

    object ListKey : NavMetadataKey<Boolean>
    object DetailKey : NavMetadataKey<Boolean>
    companion object {

        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * as a list in the [ListDetailScene].
         */
        fun listPane() = metadata {
            put(ListKey, true)
        }

        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * as a list in the [ListDetailScene].
         */
        fun detailPane() = metadata {
            put(DetailKey, true)
        }
    }
}

Wenn Sie diese ListDetailSceneStrategy in Ihrem NavDisplay verwenden möchten, ändern Sie Ihre entryProvider-Aufrufe so, dass sie ListDetailScene.listPane()-Metadaten für den Eintrag enthalten, den Sie als Listenlayout anzeigen möchten, und ListDetailScene.detailPane() für den Eintrag, den Sie als Detaillayout anzeigen möchten. Geben Sie dann ListDetailSceneStrategy() als sceneStrategy an und verlassen Sie sich auf den Standard-Fallback für Szenarien mit einem Bereich:

// Define your navigation keys
@Serializable
data object ConversationList : NavKey

@Serializable
data class ConversationDetail(val id: String) : NavKey

@Composable
fun MyAppContent() {
    val backStack = rememberNavBackStack(ConversationList)
    val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        sceneStrategies = listOf(listDetailStrategy),
        entryProvider = entryProvider {
            entry<ConversationList>(
                metadata = ListDetailSceneStrategy.listPane()
            ) {
                Column(modifier = Modifier.fillMaxSize()) {
                    Text(text = "I'm a Conversation List")
                    Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) {
                        Text(text = "Open detail")
                    }
                }
            }
            entry<ConversationDetail>(
                metadata = ListDetailSceneStrategy.detailPane()
            ) {
                Text(text = "I'm a Conversation Detail")
            }
        }
    )
}

private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) {

    // Remove any existing detail routes, then add the new detail route
    removeIf { it is ConversationDetail }
    add(detailRoute)
}

Wenn Sie keine eigene Listen-Detailansicht erstellen möchten, können Sie die Material-Listen-Detailansicht verwenden, die sinnvolle Details und Unterstützung für Platzhalter bietet, wie im nächsten Abschnitt gezeigt.

Inhalte mit Listen-Detailansicht in einer adaptiven Material-Scene anzeigen

Für den Anwendungsfall mit Listen-Detailansicht bietet das androidx.compose.material3.adaptive:adaptive-navigation3 Artefakt eine ListDetailSceneStrategy, die eine Scene mit Listen-Detailansicht erstellt. Diese Scene verarbeitet automatisch komplexe Anordnungen mit mehreren Bereichen (Liste, Details und zusätzliche Bereiche) und passt sie an die Fenstergröße und den Gerätestatus an.

So erstellen Sie eine Material-Listen-Detailansicht Scene:

  1. Abhängigkeit hinzufügen: Fügen Sie androidx.compose.material3.adaptive:adaptive-navigation3 in die Datei build.gradle.kts Ihres Projekts ein.
  2. Einträge mit ListDetailSceneStrategy Metadaten definieren: Verwenden Sie listPane(), detailPane(), und extraPane(), um Ihre NavEntrys für die entsprechende Bereichsanzeige zu kennzeichnen. Mit dem Helfer listPane() können Sie auch einen detailPlaceholder angeben, wenn kein Element ausgewählt ist.
  3. Verwenden Sie rememberListDetailSceneStrategy(): Diese zusammensetzbare Funktion bietet eine vorkonfigurierte ListDetailSceneStrategy, die von einem NavDisplay verwendet werden kann.

Das folgende Snippet ist ein Beispiel für eine Activity, in der die Verwendung von ListDetailSceneStrategy veranschaulicht wird:

@Serializable
object ProductList : NavKey

@Serializable
data class ProductDetail(val id: String) : NavKey

@Serializable
data object Profile : NavKey

class MaterialListDetailActivity : ComponentActivity() {

    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Scaffold { paddingValues ->
                val backStack = rememberNavBackStack(ProductList)
                val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()

                NavDisplay(
                    backStack = backStack,
                    modifier = Modifier.padding(paddingValues),
                    onBack = { backStack.removeLastOrNull() },
                    sceneStrategies = listOf(listDetailStrategy),
                    entryProvider = entryProvider {
                        entry<ProductList>(
                            metadata = ListDetailSceneStrategy.listPane(
                                detailPlaceholder = {
                                    ContentYellow("Choose a product from the list")
                                }
                            )
                        ) {
                            ContentRed("Welcome to Nav3") {
                                Button(onClick = {
                                    backStack.add(ProductDetail("ABC"))
                                }) {
                                    Text("View product")
                                }
                            }
                        }
                        entry<ProductDetail>(
                            metadata = ListDetailSceneStrategy.detailPane()
                        ) { product ->
                            ContentBlue("Product ${product.id} ", Modifier.background(PastelBlue)) {
                                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                                    Button(onClick = {
                                        backStack.add(Profile)
                                    }) {
                                        Text("View profile")
                                    }
                                }
                            }
                        }
                        entry<Profile>(
                            metadata = ListDetailSceneStrategy.extraPane()
                        ) {
                            ContentGreen("Profile")
                        }
                    }
                )
            }
        }
    }
}

Abbildung 1. Beispielinhalte, die in einer Material-Listen-Detailansicht ausgeführt werden.