Adaptive Layouts erstellen

Die UI für Ihre App sollte responsiv sein, um verschiedene Bildschirmgrößen, Ausrichtungen und Formfaktoren zu berücksichtigen. Ein adaptives Layout ändert sich je nach verfügbarem Bildschirmbereich. Diese reichen von einfachen Layoutanpassungen über eine Füllung von Platz bis hin zu einer vollständigen Änderung des Layouts, um zusätzlichen Platz zu nutzen.

Als deklaratives UI-Toolkit eignet sich Jetpack Compose gut zum Entwerfen und Implementieren von Layouts, die sich so anpassen, dass Inhalte in verschiedenen Größen unterschiedlich gerendert werden. Dieses Dokument enthält einige Richtlinien dazu, wie Sie die Funktion „Compose“ verwenden können, um Ihre UI responsiv zu machen.

Große Layoutänderungen für zusammensetzbare Funktionen auf Bildschirmebene explizit machen

Wenn Sie mit der Funktion „Compose“ das Layout einer gesamten Anwendung erstellen, nehmen zusammensetzbare Funktionen auf App- und Bildschirmebene den gesamten Raum ein, den Ihre App für das Rendering zur Verfügung stellt. Auf dieser Designebene kann es sinnvoll sein, das Gesamtlayout eines Bildschirms zu ändern, um größere Bildschirme optimal zu nutzen.

Vermeiden Sie bei Layoutentscheidungen die Verwendung physischer Hardwarewerte. Es mag verlockend sein, Entscheidungen auf der Grundlage eines festen Werts zu treffen (Ist das Gerät ein Tablet? Hat der physische Bildschirm ein bestimmtes Seitenverhältnis? Die Antworten auf diese Fragen sind aber möglicherweise nicht hilfreich, um zu bestimmen, welchen Raum Ihre Benutzeroberfläche bieten kann.

Ein Diagramm, das verschiedene Formfaktoren von Geräten zeigt: ein Smartphone, ein faltbares Gerät, ein Tablet und ein Laptop

Auf Tablets wird eine App möglicherweise im Mehrfenstermodus ausgeführt. Das bedeutet, dass die App den Bildschirm mit einer anderen App teilt. Unter ChromeOS befindet sich eine App möglicherweise in einem Fenster mit anpassbarer Größe. Es kann sogar mehr als einen physischen Bildschirm geben, z. B. bei einem faltbaren Gerät. In all diesen Fällen spielt die physische Bildschirmgröße keine Rolle für die Darstellung von Inhalten.

Stattdessen sollten Sie Entscheidungen basierend auf dem tatsächlichen Teil des Bildschirms treffen, der Ihrer Anwendung zugewiesen ist, z. B. die aktuellen Fenstermesswerte, die von der Jetpack-Bibliothek WindowManager bereitgestellt werden. Das JetNews-Beispiel zeigt, wie WindowManager in einer Anwendung zur Erstellung von Texten verwendet wird.

Mit diesem Ansatz wird Ihre Anwendung flexibler, da sie sich in allen oben genannten Szenarien gut verhält. Wenn du deine Layouts an den jeweiligen Bildschirmplatz anpasst, ist weniger spezielle Handhabung zur Unterstützung von Plattformen wie ChromeOS und Formfaktoren wie Tablets und faltbaren Geräten erforderlich.

Sobald Sie den relevanten verfügbaren Platz für Ihre App beobachtet haben, ist es hilfreich, die Rohgröße in eine aussagekräftige Größenklasse umzuwandeln (siehe Fenstergrößenklassen). Dabei werden Größen in Buckets in Standardgrößen gruppiert. Dabei handelt es sich um Haltepunkte, die sowohl einfache als auch flexible App-Optimierungen ermöglichen. Diese Größenklassen beziehen sich auf das Gesamtfenster Ihrer App. Verwenden Sie sie also für Layoutentscheidungen, die sich auf das gesamte Bildschirmlayout auswirken. Sie können diese Größenklassen als Zustand übergeben oder eine zusätzliche Logik ausführen, um einen abgeleiteten Zustand zu erstellen und diese an verschachtelte zusammensetzbare Funktionen zu übergeben.

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}
@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact

    // MyScreen knows nothing about window sizes, and performs logic
    // based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Bei diesem mehrschichtigen Ansatz wird die Logik für die Bildschirmgröße auf einen einzigen Ort beschränkt, anstatt sie in Ihrer Anwendung an vielen Stellen zu verteilen, die synchronisiert werden müssen. An diesem einzelnen Speicherort wird ein Status erzeugt, der genau wie jeder andere Anwendungsstatus explizit an andere zusammensetzbare Funktionen übergeben werden kann. Die explizite Übergabe des Zustands vereinfacht einzelne zusammensetzbare Funktionen, da es sich nur um normale zusammensetzbare Funktionen handelt, die die Größenklasse oder die angegebene Konfiguration zusammen mit anderen Daten annehmen.

Flexible verschachtelte zusammensetzbare Funktionen sind wiederverwendbar

Zusammensetzbare Funktionen sind wiederverwendbar, wenn sie an einer Vielzahl von Orten platziert werden können. Wenn bei einer zusammensetzbaren Funktion davon ausgegangen wird, dass sie sich immer an einem bestimmten Ort mit einer bestimmten Größe befindet, ist es schwieriger, sie an einem anderen Ort oder bei verfügbarem Speicherplatz wiederzuverwenden. Das bedeutet auch, dass einzelne, wiederverwendbare zusammensetzbare Funktionen nicht implizit von „globalen“ Größeninformationen abhängig sind.

Beispiel: Stellen Sie sich eine verschachtelte zusammensetzbare Funktion vor, die ein Listendetail-Layout implementiert, in dem ein oder zwei Bereiche nebeneinander angezeigt werden können.

Screenshot einer App, in der zwei Fenster nebeneinander zu sehen sind

Abbildung 1: Screenshot einer App mit einem typischen Listen-/Detaillayout. 1 ist der Listenbereich und 2 der Detailbereich.

Wir möchten, dass diese Entscheidung Teil des Gesamtlayouts der App ist. Daher geben wir die Entscheidung von einer zusammensetzbaren Funktion auf Bildschirmebene an, wie oben gezeigt:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

Was ist, wenn wir stattdessen möchten, dass eine zusammensetzbare Funktion ihr Layout basierend auf dem verfügbaren Platz unabhängig ändert? Zum Beispiel eine Karte, auf der zusätzliche Details angezeigt werden sollen, wenn genügend Platz vorhanden ist. Wir möchten eine Logik basierend auf einer verfügbaren Größe durchführen, aber welche Größe genau?

Beispiele für zwei verschiedene Karten: eine schmale Karte mit nur einem Symbol und Titel und eine breitere Karte mit dem Symbol, dem Titel und einer kurzen Beschreibung

Wie oben gezeigt, sollten wir nicht versuchen, die tatsächliche Bildschirmgröße des Geräts zu verwenden. Dies ist nicht für mehrere Bildschirme genau und auch nicht zutreffend, wenn die App nicht im Vollbildmodus angezeigt wird.

Da die zusammensetzbare Funktion nicht auf Bildschirmebene zusammensetzbar ist, sollten die aktuellen Fenstermesswerte auch nicht direkt verwendet werden, um die Wiederverwendbarkeit zu maximieren. Wenn die Komponente mit einem Innenrand platziert wird (z. B. für Einfügungen) oder Komponenten wie Navigations- oder App-Leisten vorhanden sind, kann der für die zusammensetzbare Funktion verfügbare Platz erheblich vom insgesamt verfügbaren Platz der App abweichen.

Daher sollte die Breite verwendet werden, die der zusammensetzbaren Funktion tatsächlich zugewiesen wird, um sich selbst zu rendern. Sie haben zwei Möglichkeiten, diese Breite zu erhalten:

Wenn Sie ändern möchten, wo oder wie Inhalte angezeigt werden, können Sie eine Sammlung von Modifikatoren oder ein benutzerdefiniertes Layout verwenden, um das Layout responsiv zu gestalten. Dies kann so einfach sein, dass ein untergeordnetes Element den gesamten verfügbaren Platz ausfüllen oder die untergeordneten Elemente mit mehreren Spalten gestalten soll, wenn genug Platz vorhanden ist.

Wenn Sie ändern möchten, was angezeigt wird, können Sie BoxWithConstraints als leistungsstärkere Alternative verwenden. Diese zusammensetzbare Funktion bietet Messeinschränkungen, mit denen Sie verschiedene zusammensetzbare Funktionen je nach verfügbarem Platz aufrufen können. Dies ist jedoch mit Kosten verbunden, da die Zusammensetzung von BoxWithConstraints bis zur Layoutphase verschoben wird, wenn diese Einschränkungen bekannt sind. Dadurch wird mehr Arbeit im Layout durchgeführt.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

Sicherstellen, dass alle Daten für verschiedene Größen verfügbar sind

Wenn Sie zusätzlichen Platz nutzen, haben Sie auf einem großen Bildschirm möglicherweise Platz, um dem Nutzer mehr Inhalte zu zeigen als auf einem kleinen Bildschirm. Wenn Sie eine zusammensetzbare Funktion mit diesem Verhalten implementieren, kann es verlockend sein, effizient zu arbeiten und Daten als Nebeneffekt der aktuellen Größe zu laden.

Dies verstößt jedoch gegen die Prinzipien des unidirektionalen Datenflusses, bei denen Daten hochgezogen und einfach für zusammensetzbare Funktionen zum korrekten Rendern bereitgestellt werden. Der zusammensetzbaren Funktion sollten genügend Daten zur Verfügung gestellt werden, damit sie unabhängig von ihrer Größe immer alles hat, was für die Anzeige erforderlich ist, selbst wenn ein Teil der Daten nicht immer verwendet wird.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

Basierend auf dem Card-Beispiel wird description immer an Card übergeben. Obwohl das description nur verwendet wird, wenn die Breite es zulässt, ist es für Card immer erforderlich, unabhängig von der verfügbaren Breite.

Durch das Übergeben von Daten werden adaptive Layouts einfacher, da sie weniger zustandsorientiert sind. Außerdem wird das Auslösen von Nebeneffekten beim Wechsel zwischen Größen vermieden (die auftreten können, wenn die Größe des Fensters, die Ausrichtung geändert oder das Gerät zusammengeklappt und aufgeklappt wird).

Dieses Prinzip ermöglicht auch die Beibehaltung des Status bei Layoutänderungen. Durch das Hochlagern von Informationen, die nicht für alle Größen verwendet werden können, können wir den Zustand des Nutzers beibehalten, wenn sich die Größe des Layouts ändert. Wir können beispielsweise das boolesche Flag showMore verwenden, damit der Status des Nutzers erhalten bleibt, wenn Größenänderungen dazu führen, dass das Layout zwischen „Ausblenden“ und „Beschreibung“ wechselt:

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

Weitere Informationen

Weitere Informationen zu benutzerdefinierten Layouts in Compose finden Sie in den folgenden zusätzlichen Ressourcen.

Beispiel-Apps

  • kanonische Layouts für große Bildschirme sind ein Repository mit bewährten Designmustern, die für eine optimale Nutzererfahrung auf Geräten mit großen Bildschirmen sorgen.
  • JetNews zeigt, wie eine App entworfen wird, bei der die Benutzeroberfläche so angepasst wird, dass der verfügbare Platz genutzt wird.
  • Antworten ist ein adaptives Beispiel zur Unterstützung von Smartphones, Tablets und faltbaren Geräten.
  • Now in Android ist eine App, die adaptive Layouts für unterschiedliche Bildschirmgrößen verwendet.

Videos