Adaptive Apps mit Jetpack Compose erstellen

1. Einführung

In diesem Codelab erfahren Sie, wie Sie adaptive Apps für Smartphones, Tablets und faltbare Geräte erstellen und wie Sie mit Jetpack Compose die Reichweite verbessern. Außerdem lernen Sie Best Practices für die Verwendung von Material 3-Komponenten und -Motiven kennen.

Bevor wir uns damit befassen, ist es wichtig zu verstehen, was wir mit Anpassungsfähigkeit meinen.

Anpassungsfähigkeit

Die Benutzeroberfläche Ihrer App sollte responsiv sein, um verschiedene Fenstergrößen, -ausrichtungen und Formfaktoren zu berücksichtigen. Ein responsives Layout ändert sich je nach verfügbarem Bildschirmplatz. Diese Änderungen reichen von einfachen Layoutanpassungen, um den verfügbaren Platz zu nutzen, über die Auswahl entsprechender Navigationsstile bis hin zum kompletten Layoutwechsel, um zusätzlichen Platz zu nutzen.

Weitere Informationen finden Sie unter Adaptives Design.

In diesem Codelab erfahren Sie, wie Sie die Anpassungsfähigkeit bei der Verwendung von Jetpack Compose nutzen und berücksichtigen können. Sie entwickeln eine Anwendung namens „Reply“, die zeigt, wie Sie die Anpassungsfähigkeit für alle Arten von Bildschirmen implementieren und wie Anpassungsfähigkeit und Erreichbarkeit zusammenwirken, um Nutzern ein optimales Erlebnis zu bieten.

Lerninhalte

  • So gestalten Sie Ihre App mit Jetpack Compose so, dass sie für alle Fenstergrößen geeignet ist.
  • Targeting Ihrer App auf verschiedene faltbare Geräte
  • Hier erfahren Sie, wie Sie verschiedene Navigationstypen für eine bessere Erreichbarkeit und Barrierefreiheit verwenden.
  • So verwenden Sie Material 3-Komponenten, um für jede Fenstergröße die beste Nutzererfahrung zu bieten.

Voraussetzungen

  • Die neueste stabile Version von Android Studio.
  • Ein veränderbares virtuelles Android 13-Gerät
  • Kenntnisse in Kotlin
  • Grundlegende Kenntnisse von Compose (z. B. die Anmerkung @Composable)
  • Grundkenntnisse zu Compose-Layouts (z. B. Row und Column)
  • Grundkenntnisse zu Modifikatoren (z. B. Modifier.padding())

Sie verwenden für dieses Codelab den veränderbaren Emulator, mit dem Sie zwischen verschiedenen Gerätetypen und Fenstergrößen wechseln können.

Vergrößerbarer Emulator mit den Optionen Smartphone, aufgefaltet, Tablet und Computer.

Wenn Sie mit Compose nicht vertraut sind, sollten Sie das Codelab zu den Grundlagen von Jetpack Compose absolvieren, bevor Sie mit diesem Codelab beginnen.

Aufgaben

  • Eine interaktive E-Mail-Client-App namens Reply mit Best Practices für anpassbare Designs, verschiedene Material-Navigationen und eine optimale Nutzung von Bildschirmfläche.

Mehrere Geräte unterstützen, was Sie in diesem Codelab erreichen

2. Einrichten

Um den Code für dieses Codelab abzurufen, klonen Sie das GitHub-Repository über die Befehlszeile:

git clone https://github.com/android/codelab-android-compose.git
cd codelab-android-compose/AdaptiveUiCodelab

Alternativ können Sie das Repository als ZIP-Datei herunterladen:

Wir empfehlen, mit dem Code im main-Branch zu beginnen und das Codelab Schritt für Schritt in Ihrem eigenen Tempo durchzugehen.

Projekt in Android Studio öffnen

  1. Wählen Sie im Fenster Willkommen bei Android Studio die Option c01826594f360d94.pngVorhandenes Projekt öffnen aus.
  2. Wählen Sie den Ordner <Download Location>/AdaptiveUiCodelab aus. Achten Sie darauf, dass Sie das Verzeichnis AdaptiveUiCodelab mit build.gradle auswählen.
  3. Nachdem Android Studio das Projekt importiert hat, testen Sie, ob Sie den Branch main ausführen können.

Startcode untersuchen

Der Code des Hauptzweigs enthält das Paket ui. Sie arbeiten mit den folgenden Dateien in diesem Paket:

  • MainActivity.kt: Einstiegspunktaktivität, bei der Sie Ihre App starten.
  • ReplyApp.kt: Enthält UI-Kompositionen für den Hauptbildschirm.
  • ReplyHomeViewModel.kt: Bietet die Daten und den UI-Status für die App-Inhalte.
  • ReplyListContent.kt: Enthält Composeables für Listen und Detailbildschirme.

Wenn Sie diese App auf einem veränderbaren Emulator ausführen und verschiedene Gerätetypen wie ein Smartphone oder Tablet ausprobieren, wird die Benutzeroberfläche nur auf den vorgegebenen Bereich erweitert, anstatt den Bildschirmplatz zu nutzen oder eine ergonomische Erreichbarkeit zu bieten.

Erster Bildschirm auf dem Smartphone

Anfangs gestreckte Ansicht auf dem Tablet

Sie aktualisieren es, um den Bildschirmplatz optimal zu nutzen, die Nutzerfreundlichkeit zu erhöhen und die Nutzererfahrung insgesamt zu verbessern.

3. Apps anpassbar machen

In diesem Abschnitt erfahren Sie, was es bedeutet, Apps anpassbar zu machen, und welche Komponenten Material 3 bietet, um dies zu vereinfachen. Außerdem werden die Bildschirmtypen und Bundesländer abgedeckt, auf die Sie Ihre Anzeigen ausrichten möchten, einschließlich Smartphones, Tablets, großer Tablets und faltbarer Smartphones.

Zuerst lernen Sie die Grundlagen von Fenstergrößen, Faltpositionen und verschiedenen Navigationsoptionen kennen. Anschließend können Sie diese APIs in Ihrer App verwenden, um sie anpassungsfähiger zu machen.

Fenstergrößen

Android-Geräte gibt es in allen Formen und Größen, von Smartphones über faltbare Geräte bis hin zu Tablets und ChromeOS-Geräten. Damit möglichst viele Fenstergrößen unterstützt werden, muss Ihre Benutzeroberfläche responsiv und anpassungsfähig sein. Damit Sie den richtigen Schwellenwert für die Änderung der Benutzeroberfläche Ihrer App finden, haben wir Haltepunktwerte definiert, mit denen Geräte in vordefinierte Größenklassen (kompakt, mittel und erweitert) klassifiziert werden können, die als Fenstergrößenklassen bezeichnet werden. Dies sind eine Reihe von subjektiven Ansichtsfenster-Bruchpunkten, die Ihnen beim Entwerfen, Entwickeln und Testen responsiver und adaptiver Anwendungslayouts helfen.

Die Kategorien wurden speziell ausgewählt, um die Einfachheit des Layouts mit der Flexibilität zu kombinieren, Ihre App für individuelle Anwendungsfälle zu optimieren. Die Fenstergrößenklasse wird immer durch den für die App verfügbaren Bildschirmbereich bestimmt. Bei Multitasking oder anderen Segmentierungen ist dies möglicherweise nicht der gesamte physische Bildschirm.

WindowWidthSizeClass für die kompakte, mittlere und maximierte Breite.

„WindowHeightSizeClass“ für die kompakte, mittlere und maximierte Höhe.

Sowohl Breite als auch Höhe werden separat klassifiziert. Ihre App hat also jederzeit zwei Fenstergrößenklassen – eine für die Breite und eine für die Höhe. Aufgrund der Allgegenwärtigkeit des vertikalen Scrollens ist die verfügbare Breite in der Regel wichtiger als die verfügbare Höhe. Daher verwenden Sie auch Breitengrößenklassen.

Zustände für das Zusammenklappen

Faltbare Geräte bieten aufgrund ihrer unterschiedlichen Größen und der vorhandenen Scharniere noch mehr Situationen, an die sich Ihre App anpassen kann. Scharniere können einen Teil des Displays verdecken, sodass dieser Bereich nicht zum Anzeigen von Inhalten geeignet ist. Sie können sich auch trennen, sodass sich beim Aufklappen des Geräts zwei separate physische Displays zeigen.

Faltbare Positionen, flach und halb geöffnet

Außerdem kann der Nutzer auf das innere Display schauen, während das Scharnier teilweise geöffnet ist. Je nach Ausrichtung des Scharniers ergeben sich unterschiedliche Körperhaltungen: Tablet-Haltung (horizontales Scharnier, rechts im Bild oben) und Buchhaltung (vertikales Scharnier).

Weitere Informationen zu Faltpositionen und Scharnieren

All diese Aspekte sollten Sie bei der Implementierung adaptiver Layouts berücksichtigen, die faltbare Geräte unterstützen.

Adaptive Informationen abrufen

Die Material3-Bibliothek adaptive bietet einfachen Zugriff auf Informationen zum Fenster, in dem Ihre App ausgeführt wird.

  1. Fügen Sie der Versionskatalogdatei Einträge für dieses Artefakt und seine Version hinzu:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. Fügen Sie in der Build-Datei des App-Moduls die neue Bibliothek als Abhängigkeit hinzu und führen Sie dann eine Gradle-Synchronisierung durch:

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

In jedem zusammensetzbaren Bereich können Sie jetzt currentWindowAdaptiveInfo() verwenden, um ein WindowAdaptiveInfo-Objekt abzurufen, das Informationen wie die aktuelle Fenstergrößenklasse und ob sich das Gerät in einer faltbaren Position wie der Tablet-Position befindet, enthält.

Du kannst das jetzt in MainActivity ausprobieren.

  1. Rufe in onCreate() innerhalb des Blocks ReplyTheme die Informationen zur Fensteranpassung ab und zeige die Größenklassen in einem Text-Komposit an. Fügen Sie Folgendes nach dem ReplyApp()-Element hinzu:

MainActivity.kt

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

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )

            val adaptiveInfo = currentWindowAdaptiveInfo()
            val sizeClassText =
                "${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
                "${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
            Text(
                text = sizeClassText,
                color = Color.Magenta,
                modifier = Modifier.padding(
                    WindowInsets.safeDrawing.asPaddingValues()
                )
            )
        }
    }
}

Wenn Sie die App jetzt ausführen, werden die Fenstergrößenklassen angezeigt, die über den App-Inhalt gedruckt werden. Sehen Sie sich gern noch einmal an, welche weiteren Optionen in den adaptiven Informationen für das Fenster verfügbar sind. Danach können Sie Text entfernen, da sie die App-Inhalte verdeckt und für die nächsten Schritte nicht erforderlich ist.

4. Dynamische Navigation

Jetzt passen Sie die Navigation der App an den Gerätestatus und die Gerätegröße an, um die App nutzerfreundlicher zu gestalten.

Wenn Nutzer ein Smartphone halten, befinden sich ihre Finger in der Regel am unteren Bildschirmrand. Wenn Nutzer ein aufgeklapptes faltbares Gerät oder ein Tablet halten, befinden sich ihre Finger in der Regel nahe den Seiten. Ihre Nutzer sollten in der Lage sein, eine App zu bedienen oder mit ihr zu interagieren, ohne extreme Handpositionen einnehmen oder ihre Handposition ändern zu müssen.

Berücksichtigen Sie beim Entwerfen Ihrer App und bei der Entscheidung, wo Sie interaktive UI-Elemente in Ihrem Layout platzieren, die ergonomischen Auswirkungen verschiedener Bereiche des Bildschirms.

  • Welche Bereiche können bequem erreicht werden, wenn das Gerät gehalten wird?
  • Welche Bereiche können nur durch Strecken der Finger erreicht werden, was möglicherweise unbequem ist?
  • Welche Bereiche sind schwer zu erreichen oder weit entfernt von der Stelle, an der der Nutzer das Gerät hält?

Die Navigation ist das Erste, mit dem Nutzer interagieren, und sie enthält wichtige Aktionen im Zusammenhang mit kritischen Nutzerinteraktionen. Daher sollte sie an Stellen platziert werden, die am einfachsten zu erreichen sind. Die adaptive Materialbibliothek bietet mehrere Komponenten, mit denen Sie die Navigation je nach Fenstergrößenklasse des Geräts implementieren können.

Untere Navigationsleiste

Die untere Navigation eignet sich perfekt für kompakte Geräte, da wir das Gerät natürlich so halten, dass unser Daumen alle Touchpunkte der unteren Navigation leicht erreichen kann. Verwenden Sie sie, wenn Sie ein kompaktes Gerät oder ein faltbares Gerät im kompakten zusammengeklappten Zustand haben.

Navigationsleiste unten mit Elementen

Bei einer mittleren Fenstergröße ist der Navigationsstreifen ideal für eine gute Erreichbarkeit, da der Daumen natürlich auf die Seite des Geräts fällt. Sie können eine Navigationsleiste auch mit einer Navigationsleiste kombinieren, um mehr Informationen anzuzeigen.

Navigationsleiste mit Elementen

Über die Navigationsleiste können Sie sich ganz einfach detaillierte Informationen zu Navigationstabs ansehen. Sie ist auf Tablets oder größeren Geräten leicht zugänglich. Es gibt zwei Arten von Navigationsleisten: eine modale Navigationsleiste und eine dauerhafte Navigationsleiste.

Modale Navigationsleiste

Sie können eine modale Navigationsleiste für kompakte bis mittelgroße Smartphones und Tablets verwenden, da sie als Overlay auf den Inhalten maximiert oder ausgeblendet werden kann. Dies kann manchmal mit einer Navigationsleiste kombiniert werden.

Modale Navigationsleiste mit Elementen

Permanente Navigationsleiste

Sie können eine dauerhafte Navigationsleiste für die feste Navigation auf großen Tablets, Chromebooks und Computern verwenden.

Permanente Navigationsleiste mit Elementen

Dynamische Navigation implementieren

Jetzt wechseln Sie je nach Gerätestatus und -größe zwischen verschiedenen Navigationstypen.

Derzeit wird in der App unabhängig vom Gerätestatus immer ein NavigationBar unter dem Bildschirminhalt angezeigt. Stattdessen können Sie die Materialkomponente NavigationSuiteScaffold verwenden, um automatisch zwischen den verschiedenen Navigationskomponenten zu wechseln, basierend auf Informationen wie der aktuellen Fenstergrößenklasse.

  1. Fügen Sie die Gradle-Abhängigkeit hinzu, um diese Komponente abzurufen. Aktualisieren Sie dazu den Versionskatalog und das Build-Script der App und führen Sie dann eine Gradle-Synchronisierung durch:

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0"

[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.navigation.suite)
}
  1. Suchen Sie in ReplyApp.kt nach der zusammensetzbaren Funktion ReplyNavigationWrapper() und ersetzen Sie die Column und ihren Inhalt durch eine NavigationSuiteScaffold:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    NavigationSuiteScaffold(
        navigationSuiteItems = {
            ReplyDestination.entries.forEach {
                item(
                    selected = it == selectedDestination,
                    onClick = { /*TODO update selection*/ },
                    icon = {
                        Icon(
                            imageVector = it.icon,
                            contentDescription = stringResource(it.labelRes)
                        )
                    },
                    label = {
                        Text(text = stringResource(it.labelRes))
                    },
                )
            }
        }
    ) {
        content()
    }
}

Das Argument navigationSuiteItems ist ein Block, mit dem Sie Elemente mit der Funktion item() hinzufügen können, ähnlich wie das Hinzufügen von Elementen in einer LazyColumn. Innerhalb des abschließenden Lambdas ruft dieser Code content() auf, das als Argument an ReplyNavigationWrapperUI() übergeben wird.

Führen Sie die App im Emulator aus und ändern Sie die Größe zwischen Smartphone, faltbarem Smartphone und Tablet. Sie sehen, dass sich die Navigationsleiste in eine Navigationsleiste verwandelt und umgekehrt.

In sehr breiten Fenstern, z. B. auf einem Tablet im Querformat, sollten Sie die dauerhafte Navigationsleiste anzeigen lassen. NavigationSuiteScaffold unterstützt das Anzeigen einer permanenten Leiste, die jedoch in keinem der aktuellen WindowWidthSizeClass-Werte angezeigt wird. Dafür ist jedoch nur eine kleine Änderung nötig.

  1. Fügen Sie den folgenden Code direkt vor dem Aufruf von NavigationSuiteScaffold ein:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    val windowSize = with(LocalDensity.current) {
        currentWindowSize().toSize().toDpSize()
    }
    val layoutType = if (windowSize.width >= 1200.dp) {
        NavigationSuiteType.NavigationDrawer
    } else {
        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
            currentWindowAdaptiveInfo()
        )
    }

    NavigationSuiteScaffold(
        layoutType = layoutType,
        ...
    ) {
        content()
    }
}

Mit diesem Code wird zuerst die Fenstergröße abgerufen und mithilfe von currentWindowSize() und LocalDensity.current in DP-Einheiten konvertiert. Anschließend wird die Fensterbreite verglichen, um den Layouttyp der Navigations-UI zu bestimmen. Wenn die Fensterbreite mindestens 1200.dp beträgt, wird NavigationSuiteType.NavigationDrawer verwendet. Andernfalls wird die Standardberechnung verwendet.

Wenn Sie die App noch einmal auf Ihrem veränderbaren Emulator ausführen und verschiedene Typen ausprobieren, stellen Sie fest, dass sich die Navigation immer dann in den für diese Größe geeigneten Typ ändert, wenn sich die Bildschirmkonfiguration ändert oder Sie ein faltbares Gerät auseinanderfalten.

Anpassungen für verschiedene Gerätegrößen werden angezeigt.

Herzlichen Glückwunsch! Sie haben verschiedene Navigationstypen kennengelernt, die verschiedene Fenstergrößen und -zustände unterstützen.

Im nächsten Abschnitt erfahren Sie, wie Sie den verbleibenden Bildschirmbereich optimal nutzen, anstatt ein Listenelement von Rand zu Rand zu ziehen.

5. Bildschirmfläche

Unabhängig davon, ob Sie die App auf einem kleinen Tablet, einem aufgeklappten Gerät oder einem großen Tablet verwenden, wird das Display so gedehnt, dass der gesamte verfügbare Platz genutzt wird. Sie sollten diesen Bildschirmbereich nutzen können, um mehr Informationen anzuzeigen, z. B. für diese App, um Nutzern auf derselben Seite E-Mails und Threads anzuzeigen.

Material 3 definiert drei kanonische Layouts, die jeweils Konfigurationen für die Fenstergrößenklassen „kompakt“, „mittel“ und „erweitert“ haben. Das kanonische Layout Listendetails eignet sich perfekt für diesen Anwendungsfall und ist beim Schreiben als ListDetailPaneScaffold verfügbar.

  1. Wenn Sie diese Komponente herunterladen möchten, fügen Sie die folgenden Abhängigkeiten hinzu und führen Sie eine Gradle-Synchronisierung durch:

gradle/libs.versions.toml

[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.layout)
    implementation(libs.androidx.material3.adaptive.navigation)
}
  1. Suchen Sie in ReplyApp.kt nach der zusammensetzbaren Funktion ReplyAppContent(). Derzeit wird durch Aufrufen von ReplyListPane() nur der Listenbereich angezeigt. Ersetzen Sie diese Implementierung durch ListDetailPaneScaffold. Fügen Sie dazu den folgenden Code ein. Da dies eine experimentelle API ist, fügen Sie auch die Annotation @OptIn in die Funktion ReplyAppContent() ein:

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            ReplyListPane(replyHomeUIState, onEmailClick)
        },
        detailPane = {
            ReplyDetailPane(replyHomeUIState.emails.first())
        }
    )
}

In diesem Code wird zuerst ein Navigationselement mit rememberListDetailPaneNavigator() erstellt. Über den Navigator können Sie steuern, welcher Bereich angezeigt wird und welcher Inhalt in diesem Bereich dargestellt werden soll. Dies wird später noch erläutert.

ListDetailPaneScaffold zeigt zwei Bereiche an, wenn die Fensterbreite maximiert ist. Andernfalls wird je nach den Werten für zwei Parameter – die Scaffold-Richtlinie und der Scaffold-Wert – der eine oder andere Bereich angezeigt. Um das Standardverhalten zu erhalten, verwendet dieser Code die Scaffold-Anweisung und den vom Navigationselement bereitgestellten Scaffold-Wert.

Die verbleibenden erforderlichen Parameter sind zusammensetzbare Lambdas für die Bereiche. ReplyListPane() und ReplyDetailPane() (unter ReplyListContent.kt) werden verwendet, um die Rollen für den Listen- und Detailbereich auszufüllen. Für ReplyDetailPane() wird ein E-Mail-Argument erwartet. Daher wird in diesem Code vorerst die erste E-Mail aus der Liste der E-Mails in ReplyHomeUIState verwendet.

Führen Sie die App aus und wechseln Sie zur Ansicht des Emulators zu faltbarem oder Tablet (möglicherweise müssen Sie auch die Ausrichtung ändern), um das zweiteilige Layout zu sehen. Das sieht schon viel besser aus als vorher.

Kommen wir nun zu einigen der gewünschten Verhaltensweisen dieses Bildschirms. Wenn der Nutzer im Listenbereich auf eine E-Mail tippt, sollte sie zusammen mit allen Antworten im Detailbereich angezeigt werden. Derzeit wird in der App nicht nachverfolgt, welche E-Mail ausgewählt wurde. Wenn Sie auf ein Element tippen, passiert nichts. Diese Informationen sollten am besten zusammen mit dem Rest des UI-Status in ReplyHomeUIState aufbewahrt werden.

  1. Öffnen Sie ReplyHomeViewModel.kt und suchen Sie die Datenklasse ReplyHomeUIState. Fügen Sie eine Property für die ausgewählte E-Mail mit dem Standardwert null hinzu:

ReplyHomeViewModel.kt

data class ReplyHomeUIState(
    val emails : List<Email> = emptyList(),
    val selectedEmail: Email? = null,
    val loading: Boolean = false,
    val error: String? = null
)
  1. In derselben Datei hat ReplyHomeViewModel eine setSelectedEmail()-Funktion, die aufgerufen wird, wenn der Nutzer auf einen Listeneintrag tippt. Ändern Sie diese Funktion, um den UI-Status zu kopieren und die ausgewählte E-Mail aufzuzeichnen:

ReplyHomeViewModel.kt

fun setSelectedEmail(email: Email) {
    _uiState.update {
        it.copy(selectedEmail = email)
    }
}

Überlegen Sie, was passiert, bevor der Nutzer auf ein Element getippt hat und die ausgewählte E-Mail-Adresse null ist. Was sollte im Detailbereich angezeigt werden? Es gibt mehrere Möglichkeiten, mit diesem Fall umzugehen, z. B. den ersten Eintrag in der Liste standardmäßig anzuzeigen.

  1. Ändern Sie in derselben Datei die Funktion observeEmails(). Wenn beim Laden der E-Mail-Liste im vorherigen UI-Status keine E-Mail ausgewählt war, legen Sie als erste E-Mail den ersten Eintrag fest:

ReplyHomeViewModel.kt

private fun observeEmails() {
    viewModelScope.launch {
        emailsRepository.getAllEmails()
            .catch { ex ->
                _uiState.value = ReplyHomeUIState(error = ex.message)
            }
            .collect { emails ->
                val currentSelection = _uiState.value.selectedEmail
                _uiState.value = ReplyHomeUIState(
                    emails = emails,
                    selectedEmail = currentSelection ?: emails.first()
                )
            }
    }
}
  1. Kehren Sie zu ReplyApp.kt zurück und füllen Sie den Detailbereich mit den Inhalten der ausgewählten E-Mail aus, sofern verfügbar:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    detailPane = {
        if (replyHomeUIState.selectedEmail != null) {
            ReplyDetailPane(replyHomeUIState.selectedEmail)
        }
    }
)

Führen Sie die App erneut aus und stellen Sie den Emulator auf die Tablet-Größe um. Wenn Sie auf ein Listenelement tippen, wird der Inhalt des Detailbereichs aktualisiert.

Das funktioniert gut, wenn beide Bereiche sichtbar sind. Wenn im Fenster aber nur Platz für einen Bereich ist, sieht es so aus, als würde beim Tippen auf ein Element nichts passieren. Versuchen Sie, die Emulator-Ansicht auf ein Smartphone oder ein faltbares Gerät im Hochformat umzustellen. Dabei wird nur der Listenbereich angezeigt, auch wenn Sie auf ein Element tippen. Das liegt daran, dass der Fokus in diesen Konfigurationen durch die ListDetailPaneScaffold immer auf dem Listenbereich liegt, auch wenn die ausgewählte E-Mail aktualisiert wird.

  1. Um dies zu beheben, füge den folgenden Code ein, wie die Lambda-Funktion an ReplyListPane übergeben wird:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    listPane = {
        ReplyListPane(
            replyHomeUIState = replyHomeUIState,
            onEmailClick = { email ->
                onEmailClick(email)
                navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
            }
        )
    },
    // ...
)

In diesem Lambda wird der zuvor erstellte Navigationspfad verwendet, um zusätzliches Verhalten hinzuzufügen, wenn auf ein Element geklickt wird. Dabei wird das ursprüngliche Lambda aufgerufen, das an diese Funktion übergeben wurde, und dann auch navigator.navigateTo(), um anzugeben, welcher Bereich angezeigt werden soll. Jedem Bereich im Gerüst ist eine Rolle zugeordnet. Für den Detailbereich ist dies ListDetailPaneScaffoldRole.Detail. In kleineren Fenstern sieht es so aus, als würde die App nach vorne springen.

Die App muss auch festlegen, was passiert, wenn der Nutzer im Detailbereich auf die Schaltfläche „Zurück“ drückt. Dieses Verhalten hängt davon ab, ob ein oder zwei Bereiche sichtbar sind.

  1. Fügen Sie den folgenden Code hinzu, um die Rückwärtsnavigation zu unterstützen.

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    BackHandler(navigator.canNavigateBack()) {
        navigator.navigateBack()
    }

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            AnimatedPane {
                ReplyListPane(
                    replyHomeUIState = replyHomeUIState,
                    onEmailClick = { email ->
                        onEmailClick(email)
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
                    }
                )
            }
        },
        detailPane = {
            AnimatedPane {
                if (replyHomeUIState.selectedEmail != null) {
                    ReplyDetailPane(replyHomeUIState.selectedEmail)
                }
            }
        }
    )
}

Der Navigationscontroller kennt den vollständigen Status der ListDetailPaneScaffold, ob eine Rücknavigation möglich ist und was in all diesen Szenarien zu tun ist. Mit diesem Code wird eine BackHandler erstellt, die aktiviert wird, wenn der Navigator zurücknavigieren kann. Im Lambda wird navigateBack() aufgerufen. Außerdem ist jeder Bereich in einem AnimatedPane()-Kompositelement verpackt, um den Übergang zwischen den Bereichen flüssiger zu gestalten.

Führen Sie die App noch einmal auf einem veränderbaren Emulator für alle verschiedenen Gerätetypen aus. Achten Sie darauf, dass sich die Navigation und der Bildschirminhalt dynamisch ändern, wenn sich die Bildschirmkonfiguration ändert oder Sie ein faltbares Gerät aufklappen. Sie können auch auf E-Mails in der Liste tippen und sehen, wie sich das Layout auf verschiedenen Bildschirmen verhält, indem Sie beide Bereiche nebeneinander anzeigen lassen oder zwischen ihnen reibungslos animiert werden.

Anpassungen für verschiedene Gerätegrößen werden angezeigt.

Herzlichen Glückwunsch! Sie haben Ihre App für alle Arten von Gerätestatus und -größen anpassbar gemacht. Probieren Sie die App auf faltbaren Geräten, Tablets oder anderen Mobilgeräten aus.

6. Glückwunsch

Glückwunsch! Sie haben dieses Codelab erfolgreich abgeschlossen und gelernt, wie Sie Apps mit Jetpack Compose adaptiv gestalten.

Sie haben gelernt, wie Sie die Größe und den Faltstatus eines Geräts prüfen und die Benutzeroberfläche, Navigation und andere Funktionen Ihrer App entsprechend aktualisieren. Außerdem haben Sie gelernt, wie sich die Anpassungsfähigkeit auf die Reichweite und die Nutzerfreundlichkeit auswirkt.

Nächste Schritte

Sehen Sie sich die anderen Codelabs auf dem Compose-Lernpfad an.

Beispielapps

Referenzdokumente