Android Entwicklung: Adaptive Compose-Layouts mit WindowSizeClass
Bernd Huber

Android Entwicklung: Adaptive Compose-Layouts mit WindowSizeClass

So migrierst du eine Smartphone-zentrierte Compose-App auf adaptive Fenstergrößen für Tablets, Foldables, Split-Screen und Desktop-Fenster.

Feste Smartphone-Abmessungen sind mit Android 16 und targetSdk 36 auf großen Displays kein verlässlicher Rahmen mehr. Der API-36-Kompatibilitätsschalter ist keine Migrationsstrategie: Er verschiebt Layoutfehler lediglich auf einen späteren Build. Eine Compose-App sollte ihre Navigation und Inhaltsbereiche deshalb aus der verfügbaren Fenstergröße ableiten.

Was ändert Android 16 auf großen Displays?

Auf Geräten mit einer kleinsten Breite von mindestens 600 dp behandelt Android 16 Apps mit targetSdk 36 grundsätzlich als größenveränderbar. Das System kann Vorgaben aus screenOrientation, resizeableActivity sowie minimalen und maximalen Seitenverhältnissen ignorieren.

Für diese Plattformregel zählt die kleinste Gerätebreite. Das Compose-Layout richtet sich dagegen nach dem aktuellen App-Fenster, das im Split-Screen oder Desktop-Modus deutlich schmaler sein kann. Bevor die UI auf Fensterklassen reagiert, müssen deshalb alte Einschränkungen aus dem zusammengeführten Manifest sichtbar werden.

Manifest auf alte Display-Beschränkungen prüfen

Abhängigkeiten und Build-Varianten können zusätzliche Activity-Einträge beisteuern. Die Ansicht Merged Manifest in Android Studio zeigt, aus welcher Manifest-Datei ein Attribut stammt. Die Suche umfasst vier Einträge:

  • android:screenOrientation legt mit Werten wie portrait oder landscape eine feste Ausrichtung fest
  • android:resizeableActivity="false" verhindert im bisherigen Verhalten die freie Größenänderung
  • android:minAspectRatio begrenzt das kleinste Seitenverhältnis
  • android:maxAspectRatio begrenzt das größte Seitenverhältnis
<application>
    <activity
        android:name=".MainActivity"
        android:screenOrientation="portrait"
        android:resizeableActivity="false"
        android:maxAspectRatio="1.86" />

    <property
        android:name="android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"
        android:value="true" />
</application>

Die Property im application-Element dient als Übergang für die API-36-Migration. Ein Migrations-Ticket sollte die betroffenen Activities, bekannte Darstellungsfehler und den Build zum Entfernen der Property nennen. Nach dieser Bestandsaufnahme wechselt die Layout-Logik vom Gerätetyp zur aktuellen Fenstergröße.

Verfügbare Fenstergröße statt Gerätetyp verwenden

Ein aufgeklapptes Foldable kann einer App im Split-Screen weniger Breite geben als ein Smartphone im Querformat. Ein frei skalierbares Tablet-Fenster durchläuft während einer Sitzung mehrere Größen. Abfragen wie isTablet oder Prüfungen eines Gerätemodells erfassen diese Übergänge nicht.

Compact

Unter 600 dp Breite zeigt die App meist eine Hauptansicht oder ein Pane zur selben Zeit.

Medium

Zwischen 600 und 839 dp steht häufig Platz für eine Navigation Rail oder zwei schmale Inhaltsbereiche bereit.

Expanded

Ab 840 dp können List-Detail-Ansichten beide Panes gleichzeitig darstellen.

Die Breitenklasse entscheidet über Navigation und Pane-Anordnung. Höhe und Gerätehaltung bleiben separate Eingaben. Ein Foldable kann beispielsweise genügend Breite für zwei Panes besitzen, während ein Scharnier die nutzbare Fläche zwischen ihnen teilt. Eine zentrale Layout-Funktion übersetzt diese Informationen in die Modi der App.

WindowSizeClass mit currentWindowAdaptiveInfo() einführen

Material 3 Adaptive stellt die aktuelle Fensterklasse und Gerätehaltung in der Composition bereit. Das Projekt benötigt die Artefakte adaptive, adaptive-layout, adaptive-navigation und material3-adaptive-navigation-suite. Ein Version Catalog hält die freigegebenen Versionen an einer Stelle:

implementation(libs.androidx.material3.adaptive)
implementation(libs.androidx.material3.adaptive.layout)
implementation(libs.androidx.material3.adaptive.navigation)
implementation(libs.androidx.material3.adaptive.navigation.suite)

Eine gemeinsame Funktion verhindert abweichende Breakpoints in einzelnen Screens:

enum class AppLayoutMode { Compact, Medium, Expanded }

@Composable
fun currentAppLayoutMode(): AppLayoutMode {
    val sizeClass = currentWindowAdaptiveInfo().windowSizeClass

    return when {
        sizeClass.isWidthAtLeastBreakpoint(
            WIDTH_DP_EXPANDED_LOWER_BOUND
        ) -> AppLayoutMode.Expanded

        sizeClass.isWidthAtLeastBreakpoint(
            WIDTH_DP_MEDIUM_LOWER_BOUND
        ) -> AppLayoutMode.Medium

        else -> AppLayoutMode.Compact
    }
}

Screens erhalten den AppLayoutMode oder daraus berechnete Parameter wie die Zahl sichtbarer Panes. Repository-Zugriffe und Fachlogik bleiben unabhängig von den Breakpoints. Auf dieselbe Fensterinformation kann nun die Navigation reagieren.

Navigation mit NavigationSuiteScaffold anpassen

Destinations, ausgewähltes Ziel und NavController bleiben bei jeder Fensterbreite identisch. NavigationSuiteScaffold verwendet standardmäßig eine Navigation Bar in kompakten Fenstern und eine Navigation Rail bei größerer Breite.

NavigationSuiteScaffold(
    navigationSuiteItems = {
        destinations.forEach { destination ->
            item(
                selected = currentRoute == destination.route,
                    navController.navigate(destination.route) {
                        launchSingleTop = true
                        restoreState = true
                    }
                },
                icon = {
                    Icon(
                        imageVector = destination.icon,
                        contentDescription = null
                    )
                },
                label = { Text(destination.label) }
            )
        }
    }
) {
    AppNavHost(navController = navController)
}

Ein gemeinsames Destinationsmodell verhindert unterschiedliche Routen und Back-Stacks für Smartphone- und Tablet-Layouts. Produktspezifische Vorgaben lassen sich über den Layout-Typ des Scaffolds abbilden, ohne einen zweiten Navigationsgraphen anzulegen. Dasselbe Prinzip trennt anschließend die Navigation einer List-Detail-Ansicht von ihrer Pane-Anordnung.

Adaptive List-Detail-Ansicht aufbauen

Nachrichten-, E-Mail- und Produkt-Apps zeigen in einem kompakten Fenster zunächst die Liste. Nach einer Auswahl übernimmt das Detail-Pane die verfügbare Fläche. In einem erweiterten Fenster stellt NavigableListDetailPaneScaffold beide Panes nebeneinander dar.

val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
val scope = rememberCoroutineScope()
val selectedItemId by viewModel.selectedItemId
    .collectAsStateWithLifecycle()

NavigableListDetailPaneScaffold(
    navigator = navigator,
    listPane = {
        AnimatedPane {
            ItemList( item ->
                    viewModel.selectItem(item.id)
                    scope.launch {
                        navigator.navigateTo(
                            ListDetailPaneScaffoldRole.Detail,
                            item.id
                        )
                    }
                }
            )
        }
    },
    detailPane = {
        AnimatedPane {
            ItemDetail(itemId = selectedItemId)
        }
    }
)

ListDetailPaneScaffold berechnet die Pane-Anordnung. Die navigierbare Variante ergänzt Pane-Navigation und die Rückwärtsnavigation. Die Jetpack-Compose-Schulung behandelt Compose-Layouts, Navigation, StateFlow und ViewModels. Damit ein Pane-Wechsel die Auswahl nicht zurücksetzt, benötigt die App eine vom Layout getrennte Zustandsquelle.

UI-State bei Größen- und Haltungsänderungen erhalten

Der ausgewählte Datensatz gehört in ein ViewModel und fließt über StateFlow in beide Layout-Varianten. Ein ViewModel übersteht eine Activity-Neuerstellung, aber keinen Prozessabbruch. Serienfähige IDs und Filter können zusätzlich in einem SavedStateHandle liegen. Dauerhafte Änderungen gehören weiterhin in eine persistente Datenquelle.

  • Das ViewModel hält die ausgewählte ID und aktive Filter unabhängig von der Pane-Anordnung
  • SavedStateHandle stellt kleine serialisierbare Zustände nach einer Prozesswiederherstellung wieder bereit
  • rememberSaveable sichert lokale Eingaben, die nicht zur Fachlogik gehören
  • rememberLazyListState verwaltet die Scrollposition einer Liste über Neuzusammensetzungen und Activity-Neuerstellungen hinweg
  • Compact und Expanded verwenden dieselbe Zustandsquelle statt eigener Datenkopien

Die Android-Clean-Architecture-Schulung vertieft ViewModels, Abhängigkeiten und unidirektionale Datenflüsse. Ob die Trennung von Layout und Zustand funktioniert, zeigt eine Testmatrix mit fortlaufenden Größenänderungen.

Split-Screen, Desktop-Fenster und Foldables testen

Ein Test in Hoch- und Querformat erfasst keine Übergänge zwischen Fensterklassen. Das Testteam zieht die Kante eines frei skalierbaren Emulatorfensters langsam über 600 und 840 dp. Navigation, Pane-Wechsel, Fokus und Scrollposition müssen während der Größenänderung erhalten bleiben.

BereichPrüfung
CompactDie App zeigt ein Pane und kehrt mit der Zurück-Navigation zum vorherigen Pane zurück
MediumDie Navigation Rail erscheint und die ausgewählte ID bleibt beim Pane-Wechsel erhalten
ExpandedListe und Detail bleiben gleichzeitig sichtbar und reagieren auf Tastatur und Maus
FoldableAufklappen, Zuklappen und Tabletop-Haltung erhalten Auswahl und Eingaben
ProzessActivity-Neuerstellung und Prozesswiederherstellung laden den vorgesehenen Zustand

TalkBack-Reihenfolge, Touch-Ziele, Predictive Back und Tastatursteuerung prüft das Team an jeder Fensterklasse. Für einen realistischen Prozess-Test wechselt die App in den Hintergrund, bevor adb shell am kill <package> ihren Prozess beendet. Die Wiederherstellung erfolgt anschließend über die Übersicht der zuletzt verwendeten Apps. Erst nach diesen Tests kann der Kompatibilitätsschalter entfallen.

API-36-Kompatibilitätsschalter entfernen

Ein eigener Migrations-Build ohne PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY macht verbliebene Abhängigkeiten von festen Smartphone-Abmessungen sichtbar. Die abschließende Prüfung umfasst diese Schritte:

  • Entferne unbegründete Ausrichtungs- und Seitenverhältnis-Vorgaben aus dem Manifest
  • Baue die App mit targetSdk 36 ohne den Kompatibilitätsschalter
  • Führe die Testmatrix für Compact, Medium, Expanded und Foldables aus
  • Vergleiche Auswahl, Eingaben und Scrollposition vor und nach jeder Größenänderung

Lege als nächsten Schritt einen separaten Migrations-Branch an und entferne dort ausschließlich den Kompatibilitätsschalter. Der erste fehlschlagende Test zeigt dann, welche Activity noch von einer festen Fensterform abhängt.

Grundlagen zu Android Studio, Android SDK, Emulatoren und Projektstruktur vermittelt die Android-App-Programmierung-Grundschulung. Weitere Schulungen bündelt die Kategorie Android-Entwicklung.