Adaptive Apps mit Jetpack Compose erstellen

1. Einführung

In diesem Codelab erfahren Sie, wie Sie adaptive Apps für Smartphones, Tablets und Faltgeräte entwickeln und wie Sie die Erreichbarkeit mit Jetpack Compose verbessern können. Außerdem erfahren Sie, wie Sie Material 3-Komponenten und ‑Theming am besten verwenden.

Bevor wir uns das genauer ansehen, ist es wichtig zu verstehen, was wir mit Anpassungsfähigkeit meinen.

Anpassungsfähigkeit

Die Benutzeroberfläche Ihrer App sollte responsiv sein, um verschiedenen Fenstergrößen, Ausrichtungen und Formfaktoren gerecht zu werden. Ein adaptives Layout ändert sich je nach verfügbarem Platz auf dem Bildschirm. Die Änderungen reichen von einfachen Layoutanpassungen, um Platz zu füllen, über die Auswahl entsprechender Navigationsstile bis hin zur vollständigen Änderung von Layouts, um zusätzlichen Platz zu nutzen.

Weitere Informationen finden Sie unter Adaptive Designs.

In diesem Codelab erfahren Sie, wie Sie die Anpassungsfähigkeit in Jetpack Compose nutzen und berücksichtigen können. Sie entwickeln die App „Reply“, mit der Sie sehen, 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 für alle Fenstergrößen.
  • So richten Sie Ihre App auf verschiedene faltbare Geräte aus.
  • Verschiedene Arten der Navigation für eine bessere Erreichbarkeit und Zugänglichkeit verwenden
  • So verwenden Sie Material 3-Komponenten, um für jede Fenstergröße die bestmögliche Nutzererfahrung zu bieten.

Voraussetzungen

In diesem Codelab verwenden Sie den Resizable Emulator, mit dem Sie zwischen verschiedenen Gerätetypen und Fenstergrößen wechseln können.

Größenanpassbarer Emulator mit Optionen für Smartphone, aufgeklappt, Tablet und Computer.

Wenn Sie mit Compose noch nicht vertraut sind, sollten Sie vor diesem Codelab das Codelab zu den Grundlagen von Jetpack Compose durcharbeiten.

Aufgaben

  • Eine interaktive E‑Mail-Client-App namens „Reply“, die Best Practices für anpassungsfähige Designs, verschiedene Material-Navigationen und eine optimale Nutzung des Bildschirmbereichs verwendet.

Unterstützung mehrerer Geräte, die Sie in diesem Codelab erreichen werden

2. Einrichten

Wenn Sie den Code für dieses Codelab abrufen möchten, 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 durchzuarbeiten.

Projekt in Android Studio öffnen

  1. Wählen Sie im Fenster Welcome to Android Studio c01826594f360d94.png Open an Existing Project 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. Wenn Android Studio das Projekt importiert hat, testen Sie, ob Sie den Branch main ausführen können.

Starter-Code ansehen

Der Code im main-Zweig enthält das Paket ui. In diesem Paket arbeiten Sie mit den folgenden Dateien:

  • MainActivity.kt: Einstiegsaktivität, über die Sie Ihre App starten.
  • ReplyApp.kt: Enthält UI-Composables für den Hauptbildschirm.
  • ReplyHomeViewModel.kt: Stellt die Daten und den UI-Status für den App-Inhalt bereit.
  • ReplyListContent.kt: Enthält Composables zum Bereitstellen von Listen und Detailansichten.

Wenn Sie diese App auf einem Emulator mit anpassbarer Größe ausführen und verschiedene Gerätetypen wie Smartphones oder Tablets ausprobieren, wird die Benutzeroberfläche einfach auf den verfügbaren Platz erweitert, anstatt den Bildschirmplatz optimal zu nutzen oder eine ergonomische Erreichbarkeit zu bieten.

Startbildschirm auf dem Smartphone

Anfängliche gestreckte Ansicht auf Tablets

Sie aktualisieren sie, um den Bildschirmplatz optimal zu nutzen, die Nutzerfreundlichkeit zu erhöhen und die User Experience insgesamt zu verbessern.

3. Apps anpassungsfähig machen

In diesem Abschnitt wird erläutert, was es bedeutet, Apps anpassungsfähig zu machen, und welche Komponenten Material 3 bietet, um dies zu erleichtern. Außerdem wird beschrieben, welche Arten von Bildschirmen und Status Sie anvisieren, darunter Smartphones, Tablets, große Tablets und faltbare Geräte.

Zuerst werden die Grundlagen zu Fenstergrößen, Faltpositionen und verschiedenen Arten von Navigationsoptionen behandelt. 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 die Benutzeroberfläche responsiv und adaptiv sein. Damit Sie den richtigen Grenzwert für die Änderung der Benutzeroberfläche Ihrer App finden, haben wir Breakpoint-Werte definiert, mit denen Geräte in vordefinierte Größenklassen (kompakt, mittel und erweitert) eingeteilt werden können. Diese werden als Fenstergrößenklassen bezeichnet. Dies sind eine Reihe von vordefinierten Viewport-Breakpoints, die Ihnen beim Entwerfen, Entwickeln und Testen von responsiven und adaptiven Anwendungslayouts helfen.

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

WindowWidthSizeClass für kompakte, mittlere und erweiterte Breite.

WindowHeightSizeClass für kompakte, mittlere und erweiterte Höhe.

Sowohl die Breite als auch die Höhe werden separat klassifiziert. Daher hat Ihre App zu jedem Zeitpunkt zwei Fenstergrößenklassen – eine für die Breite und eine für die Höhe. Die verfügbare Breite ist in der Regel wichtiger als die verfügbare Höhe, da vertikales Scrollen sehr verbreitet ist. Daher verwenden Sie in diesem Fall auch Breitenklassen.

Status „Ausgeblendet“

Faltbare Geräte bieten aufgrund ihrer unterschiedlichen Größen und der Scharniere noch mehr Möglichkeiten, Ihre App anzupassen. Scharniere können einen Teil des Displays verdecken, sodass dieser Bereich nicht für die Anzeige von Inhalten geeignet ist. Sie können auch trennend sein, d. h., dass das Gerät im aufgeklappten Zustand zwei separate physische Displays hat.

Faltbare Positionen, flach und halb geöffnet

Außerdem könnte der Nutzer auf das innere Display schauen, während das Scharnier teilweise geöffnet ist. Das führt je nach Ausrichtung des Faltens zu unterschiedlichen Körperhaltungen: Tischmodus (horizontale Faltung, rechts im Bild oben) und Buchmodus (vertikale Faltung).

Weitere Informationen zu Faltpositionen und Scharnieren

All das sind Dinge, die Sie bei der Implementierung adaptiver Layouts für faltbare Geräte berücksichtigen sollten.

Adaptive Informationen erhalten

Die Material3-Bibliothek adaptive bietet bequemen 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 der Build-Datei des App-Moduls die neue Bibliotheksabhä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 mit Informationen wie der aktuellen Fenstergrößenklasse und dem Status des Geräts (z. B. ob es sich in einer faltbaren Position wie der Tischmodus befindet) abzurufen.

Sie können das jetzt in MainActivity ausprobieren.

  1. Rufen Sie in onCreate() innerhalb des ReplyTheme-Blocks die Informationen zur Fensteranpassung ab und zeigen Sie die Größenklassen in einem Text-Composable an. Sie können dies nach dem ReplyApp()-Element hinzufügen:

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 über dem App-Inhalt ausgegeben. Sehen Sie sich an, was sonst noch in den adaptiven Informationen im Fenster enthalten ist. Anschließend können Sie Text entfernen, da es den App-Inhalt abdeckt und für die nächsten Schritte nicht erforderlich ist.

4. Dynamische Navigation

Nun passen Sie die Navigation der App an den Gerätestatus und die Gerätegröße an, um die Bedienung der App zu vereinfachen.

Wenn Nutzer ein Smartphone halten, befinden sich ihre Finger normalerweise am unteren Bildschirmrand. Wenn Nutzer ein aufgeklapptes faltbares Gerät oder ein Tablet halten, befinden sich ihre Finger in der Regel in der Nähe der Seiten. Nutzer sollten in der Lage sein, in einer App zu navigieren oder eine Interaktion zu starten, ohne dass sie ihre Hände in extreme Positionen bringen oder ihre Handpositionen ändern müssen.

Berücksichtigen Sie bei der Gestaltung Ihrer App und der Entscheidung, wo interaktive UI-Elemente in Ihrem Layout platziert werden sollen, die ergonomischen Auswirkungen verschiedener Bildschirmbereiche.

  • Welche Bereiche sind beim Halten des Geräts gut erreichbar?
  • 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, womit Nutzer interagieren. Sie enthält wichtige Aktionen im Zusammenhang mit kritischen Nutzeraktionen und sollte daher an Stellen platziert werden, die am einfachsten zu erreichen sind. Die adaptive Material-Bibliothek bietet mehrere Komponenten, mit denen Sie die Navigation je nach Fenstergrößenklasse des Geräts implementieren können.

Navigation am unteren Rand

Die untere Navigation eignet sich perfekt für kompakte Geräte, da wir das Gerät in der Regel so halten, dass unser Daumen alle Touchelemente der unteren Navigation leicht erreichen kann. Verwenden Sie es immer dann, wenn Sie ein kompaktes Gerät oder ein faltbares Gerät im zusammengeklappten Zustand haben.

Navigationsleiste am unteren Rand mit Elementen

Bei einer mittleren Fensterbreite ist die Navigationsleiste ideal für die Erreichbarkeit, da der Daumen auf natürliche Weise an der Seite des Geräts liegt. Sie können auch eine Navigationsleiste mit einer Navigationsleiste kombinieren, um weitere Informationen anzuzeigen.

Navigationsleiste mit Elementen

Über die Navigationsleiste können Sie ganz einfach detaillierte Informationen zu den Navigationstabs aufrufen. Sie ist besonders praktisch, wenn Sie Tablets oder größere Geräte verwenden. Es gibt zwei Arten von Navigationsleisten: modale und permanente Navigationsleisten.

Modale Navigationsleiste

Für Smartphones und Tablets mit kompakter bis mittlerer Größe können Sie einen modalen Navigationsbereich verwenden, da er als Overlay über den Inhalten ein- oder ausgeblendet werden kann. Manchmal wird sie mit einer Navigationsleiste kombiniert.

Modale Navigationsleiste mit Elementen

Permanente Navigationsleiste

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

Permanente Navigationsleiste mit Elementen

Dynamische Navigation implementieren

Nun wechseln Sie zwischen verschiedenen Arten der Navigation, wenn sich der Gerätestatus und die Größe ändern.

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

  1. Fügen Sie die Gradle-Abhängigkeit hinzu, um diese Komponente zu erhalten. Aktualisieren Sie dazu den Versionskatalog und das Build-Skript 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 Column und den Inhalt durch 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 mithilfe der Funktion item() hinzufügen können. Das ist ähnlich wie beim Hinzufügen von Elementen in einem LazyColumn. Im nachgestellten Lambda ruft dieser Code die content() auf, die als Argument an ReplyNavigationWrapperUI() übergeben wird.

Führen Sie die App auf dem Emulator aus und versuchen Sie, die Größe zwischen Smartphone, Falt- und Tablet zu ändern. Sie werden sehen, dass sich die Navigationsleiste in eine Navigationsspalte ändert und wieder zurück.

Bei sehr breiten Fenstern, z. B. auf einem Tablet im Querformat, kann es sinnvoll sein, die permanente Navigationsleiste zu verwenden. NavigationSuiteScaffold unterstützt die Anzeige eines permanenten Bereichs, der jedoch in keinem der aktuellen WindowWidthSizeClass-Werte angezeigt wird. Mit einer kleinen Änderung können Sie das jedoch erreichen.

  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()
    }
}

In diesem Code wird zuerst die Fenstergröße abgerufen und mit 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 auf die Standardberechnung zurückgegriffen.

Wenn Sie die App noch einmal auf Ihrem Emulator mit anpassbarer Größe ausführen und verschiedene Typen ausprobieren, sehen Sie, dass sich die Navigation immer dann ändert, wenn sich die Bildschirmkonfiguration ändert oder Sie ein faltbares Gerät aufklappen.

Es werden Änderungen an der Anpassungsfähigkeit für Geräte unterschiedlicher Größe gezeigt.

Herzlichen Glückwunsch! Sie haben verschiedene Arten der Navigation kennengelernt, die unterschiedliche Fenstergrößen und ‑status unterstützen.

Im nächsten Abschnitt erfahren Sie, wie Sie den verbleibenden Bildschirmbereich nutzen können, anstatt dasselbe Listenelement über die gesamte Breite zu dehnen.

5. Bildschirmplatznutzung

Egal, ob Sie die App auf einem kleinen Tablet, einem aufgeklappten Gerät oder einem großen Tablet ausführen – der Bildschirm wird so gestreckt, dass der verbleibende Platz ausgefüllt wird. Sie möchten diesen Platz nutzen, um mehr Informationen zu präsentieren, z. B. E-Mails und Threads auf derselben Seite.

Material 3 definiert drei kanonische Layouts, die jeweils Konfigurationen für die Fenstergrößenklassen „Kompakt“, „Mittel“ und „Erweitert“ haben. Das kanonische Layout List Detail ist für diesen Anwendungsfall ideal und in Compose als ListDetailPaneScaffold verfügbar.

  1. Sie erhalten diese Komponente, indem Sie die folgenden Abhängigkeiten hinzufügen und eine Gradle-Synchronisierung durchführen:

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(), in der derzeit nur der Listenbereich durch Aufrufen von ReplyListPane() angezeigt wird. Ersetzen Sie diese Implementierung durch ListDetailPaneScaffold, indem Sie den folgenden Code einfügen. Da es sich um eine experimentelle API handelt, fügen Sie der Funktion ReplyAppContent() auch die Annotation @OptIn hinzu:

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())
        }
    )
}

Mit diesem Code wird zuerst ein Navigator mit rememberListDetailPaneNavigator() erstellt. Über den Navigator können Sie steuern, welcher Bereich angezeigt wird und welche Inhalte in diesem Bereich dargestellt werden sollen. Das wird später noch genauer erläutert.

ListDetailPaneScaffold zeigt zwei Bereiche an, wenn die Fensterbreiten-Größenklasse erweitert wird. Andernfalls wird je nach den Werten, die für zwei Parameter angegeben wurden, der eine oder der andere Bereich angezeigt: die Scaffold-Anweisung und der Scaffold-Wert. Um das Standardverhalten zu erhalten, wird in diesem Code die Scaffold-Anweisung und der vom Navigator bereitgestellte Scaffold-Wert verwendet.

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

Führen Sie die App aus und wechseln Sie die Emulatoransicht zu „Foldable“ oder „Tablet“ (möglicherweise müssen Sie auch die Ausrichtung ändern), um das Layout mit zwei Bereichen zu sehen. Das sieht schon viel besser aus als vorher.

Sehen wir uns nun einige der gewünschten Verhaltensweisen dieses Bildschirms an. 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 erfasst, welche E-Mail-Adresse ausgewählt wurde. Wenn Sie auf ein Element tippen, passiert nichts. Am besten speichern Sie diese Informationen zusammen mit dem restlichen UI-Status in ReplyHomeUIState.

  1. Öffnen Sie ReplyHomeViewModel.kt und suchen Sie nach der Datenklasse ReplyHomeUIState. Fügen Sie für die ausgewählte E-Mail-Adresse eine Property 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 ein Listenelement tippt. Ändern Sie diese Funktion, um den UI-Status zu kopieren und die ausgewählte E-Mail-Adresse aufzuzeichnen:

ReplyHomeViewModel.kt

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

Sie sollten auch berücksichtigen, was passiert, bevor der Nutzer auf ein Element tippt und die ausgewählte E-Mail-Adresse null ist. Was soll im Detailbereich angezeigt werden? Es gibt mehrere Möglichkeiten, diesen Fall zu behandeln, z. B. indem standardmäßig das erste Element in der Liste angezeigt wird.

  1. Ändern Sie in derselben Datei die Funktion observeEmails(). Wenn die Liste der E-Mail-Adressen geladen wird und im vorherigen UI-Status keine E-Mail-Adresse ausgewählt war, legen Sie die erste E-Mail-Adresse als ausgewählte E-Mail-Adresse 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 verwenden Sie die ausgewählte E‑Mail-Adresse, sofern verfügbar, um den Inhalt des Detailbereichs zu generieren:

ReplyApp.kt

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

Führen Sie die App noch einmal aus und stellen Sie den Emulator auf Tabletgröß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 jedoch nur Platz für einen Bereich ist, sieht es so aus, als würde nichts passieren, wenn Sie auf ein Element tippen. Wenn Sie die Emulatoransicht auf ein Smartphone oder ein faltbares Gerät im Hochformat umstellen, ist auch nach dem Tippen auf ein Element nur der Listenbereich sichtbar. Das liegt daran, dass der Fokus in diesen Konfigurationen weiterhin auf dem Listenbereich liegt, obwohl die ausgewählte E‑Mail-Adresse aktualisiert wird.ListDetailPaneScaffold

  1. Fügen Sie dazu den folgenden Code als Lambda ein, das an ReplyListPane übergeben wird:

ReplyApp.kt

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

Diese Lambda-Funktion verwendet den zuvor erstellten Navigator, um zusätzliches Verhalten hinzuzufügen, wenn auf ein Element geklickt wird. Sie ruft das ursprüngliche Lambda auf, das an diese Funktion übergeben wurde, und dann auch navigator.navigateTo(), um anzugeben, welcher Bereich angezeigt werden soll. Jeder Bereich im Gerüst hat eine zugeordnete Rolle. Für den Detailbereich ist es ListDetailPaneScaffoldRole.Detail. Bei kleineren Fenstern wird dadurch der Eindruck erweckt, dass die App vorwärts navigiert wurde.

Die App muss auch verarbeiten, 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 Navigator kennt den vollständigen Status von ListDetailPaneScaffold, weiß, ob die Zurück-Navigation möglich ist, und weiß, was in all diesen Szenarien zu tun ist. Mit diesem Code wird ein BackHandler erstellt, das immer dann aktiviert wird, wenn der Navigator zurück navigieren kann. Im Lambda wird navigateBack() aufgerufen. Außerdem ist jede Ansicht in ein AnimatedPane()-Composable eingebunden, um den Übergang zwischen den Ansichten zu optimieren.

Führen Sie die App noch einmal auf einem Emulator mit anpassbarer Größe für alle verschiedenen Gerätetypen aus. Sie werden feststellen, dass sich die Navigation und der Bildschirminhalt dynamisch an die Änderungen des Gerätestatus anpassen, wenn sich die Bildschirmkonfiguration ändert oder Sie ein faltbares Gerät aufklappen. Tippen Sie auch auf E‑Mails im Listenbereich und sehen Sie sich an, wie sich das Layout auf verschiedenen Bildschirmen verhält. Auf einigen Bildschirmen werden beide Bereiche nebeneinander angezeigt, auf anderen wird zwischen ihnen animiert.

Es werden Änderungen an der Anpassungsfähigkeit für Geräte unterschiedlicher Größe gezeigt.

Herzlichen Glückwunsch! Sie haben Ihre App erfolgreich für alle Arten von Gerätestatus und ‑größen angepasst. 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 anpassungsfähig machen.

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

Nächste Schritte

Sehen Sie sich die anderen Codelabs im Compose-Lernpfad an.

Beispielanwendungen

  • Die Compose-Beispiele sind eine Sammlung vieler Apps, die die in Codelabs erläuterten Best Practices enthalten.

Referenzdokumente