Skip to Content
Mobile Apps2 - AndroidCompose-Grundlagen
Folienvorschau
Dauer
20 Minuten
Ziel
Die Bausteine von Jetpack Compose kennenlernen

Themen

  • Composables und Preview
  • Column, Row, Box und Modifier
  • data class und Fake-Daten
  • Scaffold und TopAppBar
Klicken für Tastaturnavigation · F für Vollbild
Präsentation starten · 20 Minuten ↗

Compose-Grundlagen

Wir bauen die Oberfläche der Wetter-App Schritt für Schritt mit Jetpack Compose auf. Am Ende dieser Seite zeigt die App das Wetter einer Stadt mit Fake-Daten an.

Code: Repository auf GitHub , Schritte 0 bis 4.

Die Notizen enthalten den vollständigen Code jeder Datei, sodass sich die App durch Kopieren und Einfügen nachbauen lässt. Die Folien zeigen nur Ausschnitte. Wo auch die Notizen kürzen, verlinken sie die Datei im Repository mit dem Tag des Schritts. Wer das Projekt anders benannt hat, passt die Zeilen mit package und import com.example… an.

Die Wetter-App

Eine Liste von Städten, nach Antippen das aktuelle Wetter der Stadt.

  • Schritte 1–8: Oberfläche und Architektur mit Fake-Daten
  • Schritt 9: echte Wetterdaten von Open-Meteo, angebunden mit einem Coding Agent
  • Schritt 10: Feinschliff mit Material 3
  • Jeder Schritt ist ein Commit mit Tag, z. B. schritt-05

Ausgangspunkt (schritt-00) ist ein neues Projekt mit der Vorlage Empty Activity und dem Namen WeatherAndroidTIF24. Aus dem Projektnamen erzeugt Android Studio den Namen des Themes (WeatherAndroidTIF24Theme) und des Pakets (com.example.weatherandroidtif24); bei einem anderen Namen heißen sie entsprechend anders. Wer nicht selbst tippen möchte, klont das Repository und wechselt mit git checkout schritt-00 an den Anfang.

In gradle/libs.versions.toml markiert Android Studio Versionen gelb, für die es eine neuere gibt; Alt+Enter setzt sie ein. Das Referenz-Repository verwendet die Versionen der Vorlage. Aktualisieren ist möglich, kann aber weitere Anpassungen nötig machen, z. B. eine neuere Version des Android-Gradle-Plugins.

In MainActivity bleibt nur das Nötigste übrig:

MainActivity.kt
package com.example.weatherandroidtif24 import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.example.weatherandroidtif24.ui.theme.WeatherAndroidTIF24Theme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { WeatherAndroidTIF24Theme { } } } }

setContent { … } legt fest, welche Composables die Activity anzeigt. Die geschweiften Klammern sind ein Trailing Lambda: Ist der letzte Parameter einer Funktion eine Funktion, darf sie hinter den runden Klammern stehen. setContent { … } ist also kurz für setContent(content = { … }). Diese Schreibweise steckt in fast jedem Compose-Code, z. B. Column { … } oder Scaffold(…) { innerPadding -> … }.

Zusätzlich liegt im Repository eine Datei AGENTS.md mit Regeln für Coding Agents (siehe Navigation und API).

Ohne lokale Installation geht es auch im Browser: Google AI Studio  erzeugt aus einem Prompt ein Android-Projekt mit Kotlin, Compose und ViewModel, wie wir es hier aufbauen. Die App läuft in einem Cloud-Emulator im Browser oder per USB auf dem eigenen Handy; das Projekt lässt sich als ZIP herunterladen und in Android Studio weiterbearbeiten. Grenzen: nur ein Modul, kein Firebase, kein NDK. Gute Übung: die Wetter-App per Prompt erzeugen lassen und mit unserem Code vergleichen.

Projektstruktur

Der Kotlin-Code ist nach Aufgaben in Pakete aufgeteilt.

app/src/main/java/com/example/weatherandroidtif24/ ├── MainActivity.kt ├── ui/ Screens und Composables, ViewModels │ └── theme/ Farben, Schriften, Theme (aus der Vorlage) ├── model/ Datenmodell: City, CurrentWeather └── data/ Datenquellen: Fake-Daten, Repositorys └── api/ HTTP-Client und DTOs (ab Schritt 9)

Pfade auf dieser und den folgenden Seiten (z. B. ui/WeatherScreen.kt) beziehen sich auf diesen Ordner. In der Ansicht Android von Android Studio erscheint er unter app/kotlin+java/com.example.weatherandroidtif24.

Neue Pakete und Dateien legen wir per Rechtsklick auf das Paket an:

  • New → Package für einen Unterordner wie model
  • New → Kotlin Class/File für eine Datei, dann z. B. File wählen

ui/ mit ui/theme/ bringt die Vorlage Empty Activity schon mit; die übrigen Pakete entstehen in Schritt 3 (model, data) und Schritt 9 (data/api).

Tipps für Android Studio

AktionmacOSWindows/Linux
Quick Fix, z. B. fehlender Import⌥ ⏎Alt+Enter
Imports aufräumen (Optimize Imports)⌃ ⌥ OCtrl+Alt+O
Code formatieren (Reformat Code)⌥ ⌘ LCtrl+Alt+L
Umbenennen, überall (Rename)⇧ F6Shift+F6
Zur Definition springen⌘ + KlickCtrl+Klick
Parameter einer Funktion anzeigen⌘ PCtrl+P
Alles durchsuchen2× ⇧2× Shift

Live Templates sparen Tipparbeit: comp + Tab erzeugt ein Composable, prev + Tab eine Preview.

Damit Android Studio eindeutige Imports selbst einfügt: Settings → Editor → General → Auto Import → „Add unambiguous imports on the fly“.

Composables

Ein Composable ist eine Kotlin-Funktion, die beschreibt, wie ein Teil der Oberfläche aussieht.

Neue Datei ui/WeatherScreen.kt im Paket ui, das die Vorlage schon mitbringt:

@Composable fun WeatherScreen(modifier: Modifier = Modifier) { Text(text = "Lörrach", modifier = modifier) Text(text = "14 °C", modifier = modifier) }

Compose arbeitet deklarativ: Wir beschreiben, was angezeigt werden soll, nicht wie die Oberfläche Schritt für Schritt verändert wird. Ändern sich die Daten, ruft Compose die Funktion erneut auf und zeichnet das Ergebnis neu.

Ausblick: Dasselbe Prinzip heißt in Flutter Widget, in React Native Komponente. Die Oberfläche entsteht immer als Baum verschachtelter Bausteine:

FrameworkBausteinBeispiel
ComposeComposableColumn { Text("Lörrach") }
FlutterWidgetColumn(children: [Text('Lörrach')])
React NativeKomponente<View><Text>Lörrach</Text></View>

Konventionen für Composables:

  • Annotation @Composable
  • Name in PascalCase, wie eine Klasse
  • kein Rückgabewert: Composables erzeugen UI, statt etwas zurückzugeben
  • ein optionaler Parameter modifier: Modifier = Modifier: Standardwert ist der leere Modifier; so kann der Aufrufer z. B. Abstände von außen festlegen. Das Composable gibt ihn an sein äußerstes Element weiter.

Damit WeatherScreen auch in der App erscheint, ruft MainActivity es auf:

Code: MainActivity.kt (schritt-01)

MainActivity.kt
package com.example.weatherandroidtif24 import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.ui.Modifier import com.example.weatherandroidtif24.ui.WeatherScreen import com.example.weatherandroidtif24.ui.theme.WeatherAndroidTIF24Theme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { WeatherAndroidTIF24Theme { // Abstand zu Statusleiste und Kamera-Aussparung WeatherScreen(modifier = Modifier.safeDrawingPadding()) } } } }

Neue Projekte zeichnen edge-to-edge, also bis unter die Statusleiste. safeDrawingPadding() hält den Inhalt aus diesen Bereichen heraus.

Die beiden Texte überlappen sich, weil noch ein Layout fehlt. Darum geht es im nächsten Abschnitt.

Warum überlappen sich die Texte? Anders als in HTML ordnet Compose Elemente nicht automatisch untereinander an. Ohne Layout landen alle Kinder an derselben Stelle oben links; die Anordnung legt erst ein Layout wie Column fest. Deshalb bekommen hier auch beide Texts den modifier: Es gibt noch kein äußerstes Element, an das er gehen könnte.

Mit @Preview zeigt Android Studio ein Composable direkt im Editor an, ohne Emulator.

@Preview(showBackground = true) @Composable fun WeatherScreenPreview() { WeatherScreen() }

Die Preview aktualisiert sich bei jeder Änderung im Code. Für Interaktionen (Antippen, Eingaben) gibt es den interaktiven Modus der Preview; für alles andere starten wir die App im Emulator.

Die Preview steht in derselben Datei unter WeatherScreen. Mit den Imports sieht die Datei so aus:

Code: ui/WeatherScreen.kt (schritt-01)

ui/WeatherScreen.kt
package com.example.weatherandroidtif24.ui import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @Composable fun WeatherScreen(modifier: Modifier = Modifier) { Text(text = "Lörrach", modifier = modifier) Text(text = "14 °C", modifier = modifier) } @Preview(showBackground = true) @Composable fun WeatherScreenPreview() { WeatherScreen() }

Code: schritt-01 · Änderungen 

Layouts: Column, Row, Box

Composables werden mit Layout-Composables angeordnet.

  • Column: untereinander
  • Row: nebeneinander
  • Box: übereinander, z. B. Inhalt auf einem Hintergrund
  • Spacer: Abstand bzw. freier Platz

Die Verschachtelung im Code entspricht der Verschachtelung auf dem Bildschirm.

Layout als verschachtelte Boxen: Bildschirm und Code haben denselben Aufbau

Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = "Lörrach", fontSize = 28.sp) Text(text = "⛅", fontSize = 72.sp) Text(text = "14 °C", fontSize = 48.sp) Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.fillMaxWidth()) { Text(text = "Wind: 8 km/h") Spacer(modifier = Modifier.weight(1f)) Text(text = "Teilweise bewölkt") } }

Dieser Code steckt in einem neuen Composable CurrentWeatherCard, ebenfalls in ui/WeatherScreen.kt. WeatherScreen ruft es nur noch auf und reicht seinen modifier weiter:

ui/WeatherScreen.kt
@Composable fun WeatherScreen(modifier: Modifier = Modifier) { CurrentWeatherCard(modifier = modifier) }

CurrentWeatherCard legt eine Box mit Hintergrund um die Column (siehe nächster Abschnitt zu Modifiern).

Größen werden in dp (density-independent pixels) angegeben, damit sie auf allen Displays gleich groß wirken. Schriftgrößen in sp (scalable pixels): wie dp, berücksichtigen aber zusätzlich die Schriftgröße aus den Systemeinstellungen.

Bei Alt+Enter auf den richtigen Import achten: androidx.compose.ui.Modifier (nicht java.lang.reflect.Modifier) und androidx.compose.ui.graphics.Color (nicht android.graphics.Color).

Spacer(Modifier.weight(1f)) nimmt den gesamten freien Platz in der Row ein und schiebt so den zweiten Text an den rechten Rand.

Layouts lassen sich beliebig verschachteln. Im Unterschied zu älteren UI-Systemen ist das in Compose kein Performance-Problem.

Modifier

Modifier verändern Größe, Abstand, Hintergrund und Verhalten eines Composables. Sie werden durch aufeinanderfolgende Funktionsaufrufe verkettet.

Box( modifier = modifier .fillMaxWidth() .padding(16.dp) .background(Color(0xFFBBDEFB), RoundedCornerShape(24.dp)) .padding(24.dp), contentAlignment = Alignment.Center, ) { /* Column von oben */ }

Die Reihenfolge zählt: padding vor background ist Außenabstand, padding nach background ist Innenabstand.

Die Kette beginnt mit dem Parameter modifier statt mit Modifier. So erreicht der Modifier des Aufrufers (z. B. safeDrawingPadding() aus MainActivity) das äußerste Element, statt verloren zu gehen.

Am besten in der Preview ausprobieren: die beiden padding-Aufrufe vertauschen oder einen entfernen und den Unterschied ansehen.

Weitere häufige Modifier: size, fillMaxSize, clickable, border, clip.

Die ganze Datei nach diesem Schritt, mit Box, Column und Row:

Code: ui/WeatherScreen.kt (schritt-02)

ui/WeatherScreen.kt
package com.example.weatherandroidtif24.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable fun WeatherScreen(modifier: Modifier = Modifier) { CurrentWeatherCard(modifier = modifier) } @Composable fun CurrentWeatherCard(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxWidth() .padding(16.dp) .background(Color(0xFFBBDEFB), RoundedCornerShape(24.dp)) .padding(24.dp), contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = "Lörrach", fontSize = 28.sp) Text(text = "⛅", fontSize = 72.sp) Text(text = "14 °C", fontSize = 48.sp) Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.fillMaxWidth()) { Text(text = "Wind: 8 km/h") Spacer(modifier = Modifier.weight(1f)) Text(text = "Teilweise bewölkt") } } } } @Preview(showBackground = true) @Composable fun WeatherScreenPreview() { WeatherScreen() }

Code: schritt-02 · Änderungen 

Datenmodell mit data class

Statt fester Texte bekommen Composables nun Daten als Parameter.

data class City( val name: String, val latitude: Double, val longitude: Double, ) data class CurrentWeather( val temperature: Double, val windSpeed: Double, val condition: WeatherCondition, )

Die beiden Klassen kommen in zwei Dateien im neuen Paket model:

model/City.kt
package com.example.weatherandroidtif24.model data class City( val name: String, val latitude: Double, val longitude: Double, )
model/CurrentWeather.kt
package com.example.weatherandroidtif24.model enum class WeatherCondition { CLEAR, PARTLY_CLOUDY, CLOUDY, FOG, RAIN, SNOW, THUNDERSTORM } data class CurrentWeather( val temperature: Double, val windSpeed: Double, val condition: WeatherCondition, )

Eine data class bekommt automatisch equals, toString und copy. Ihre Properties sind mit val nur lesbar; Getter erzeugt Kotlin automatisch, Setter gibt es nur bei var. Für feste Auswahlmöglichkeiten wie die Wetterlage eignet sich eine enum class. Sie steht mit in model/CurrentWeather.kt.

Die Koordinaten in City brauchen wir erst in Schritt 9 für die API. Das Datenmodell ist aber von Anfang an so aufgebaut, dass es später passt.

Die Fake-Daten kommen in das neue Paket data: eine Liste von Städten und eine Map, die Städtenamen das Wetter zuordnet.

data/FakeData.kt
package com.example.weatherandroidtif24.data import com.example.weatherandroidtif24.model.City import com.example.weatherandroidtif24.model.CurrentWeather import com.example.weatherandroidtif24.model.WeatherCondition val fakeCities: List<City> = listOf( City(name = "Lörrach", latitude = 47.6156, longitude = 7.6614), City(name = "Freiburg", latitude = 47.9990, longitude = 7.8421), City(name = "Basel", latitude = 47.5596, longitude = 7.5886), ) val fakeCurrentWeather: Map<String, CurrentWeather> = mapOf( "Lörrach" to CurrentWeather( temperature = 14.2, windSpeed = 8.5, condition = WeatherCondition.PARTLY_CLOUDY, ), "Freiburg" to CurrentWeather( temperature = 15.0, windSpeed = 6.1, condition = WeatherCondition.CLEAR, ), "Basel" to CurrentWeather( temperature = 13.6, windSpeed = 11.2, condition = WeatherCondition.RAIN, ), )

Composables sollten Daten als Parameter erhalten und sie nicht eigenständig holen.

@Composable fun CurrentWeatherCard( city: City, weather: CurrentWeather, modifier: Modifier = Modifier, ) { // ... Text(text = city.name) Text(text = weather.condition.emoji()) Text(text = "${weather.temperature.roundToInt()} °C") }

Solche Composables heißen stateless: Sie zeigen nur an, was sie als Parameter bekommen. Dadurch lassen sie sich in der Preview mit beliebigen Daten testen.

Die Anzeige der Wetterlage ist eine Erweiterungsfunktion mit when in einer neuen Datei. Sie gehört zur UI, nicht ins Datenmodell:

ui/WeatherConditionUi.kt
package com.example.weatherandroidtif24.ui import com.example.weatherandroidtif24.model.WeatherCondition fun WeatherCondition.emoji(): String = when (this) { WeatherCondition.CLEAR -> "☀️" WeatherCondition.PARTLY_CLOUDY -> "⛅" WeatherCondition.CLOUDY -> "☁️" WeatherCondition.FOG -> "🌫️" WeatherCondition.RAIN -> "🌧️" WeatherCondition.SNOW -> "❄️" WeatherCondition.THUNDERSTORM -> "⛈️" } fun WeatherCondition.label(): String = when (this) { WeatherCondition.CLEAR -> "Sonnig" WeatherCondition.PARTLY_CLOUDY -> "Teilweise bewölkt" WeatherCondition.CLOUDY -> "Bewölkt" WeatherCondition.FOG -> "Nebel" WeatherCondition.RAIN -> "Regen" WeatherCondition.SNOW -> "Schnee" WeatherCondition.THUNDERSTORM -> "Gewitter" }

when muss alle Fälle der enum class abdecken. Kommt ein neuer Fall hinzu, meldet der Compiler jede Stelle, die angepasst werden muss. label() liefert den Text („Sonnig“, „Regen“ …) für die rechte Seite der Row; links steht die Windgeschwindigkeit weather.windSpeed.

Auch WeatherScreen bekommt die Daten jetzt als Parameter und reicht sie an die Karte weiter. Die Preview übergibt Fake-Daten. roundToInt() rundet auf eine ganze Zahl, "${…}" setzt einen Ausdruck in einen String ein.

Code: ui/WeatherScreen.kt (schritt-03)

ui/WeatherScreen.kt
package com.example.weatherandroidtif24.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.weatherandroidtif24.data.fakeCities import com.example.weatherandroidtif24.data.fakeCurrentWeather import com.example.weatherandroidtif24.model.City import com.example.weatherandroidtif24.model.CurrentWeather import kotlin.math.roundToInt @Composable fun WeatherScreen(city: City, weather: CurrentWeather, modifier: Modifier = Modifier) { CurrentWeatherCard(city = city, weather = weather, modifier = modifier) } @Composable fun CurrentWeatherCard(city: City, weather: CurrentWeather, modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxWidth() .padding(16.dp) .background(Color(0xFFBBDEFB), RoundedCornerShape(24.dp)) .padding(24.dp), contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = city.name, fontSize = 28.sp) Text(text = weather.condition.emoji(), fontSize = 72.sp) Text(text = "${weather.temperature.roundToInt()} °C", fontSize = 48.sp) Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.fillMaxWidth()) { Text(text = "Wind: ${weather.windSpeed.roundToInt()} km/h") Spacer(modifier = Modifier.weight(1f)) Text(text = weather.condition.label()) } } } } @Preview(showBackground = true) @Composable fun WeatherScreenPreview() { val city = fakeCities.first() WeatherScreen(city = city, weather = fakeCurrentWeather.getValue(city.name)) }

Auch MainActivity übergibt jetzt die Fake-Daten:

Code: MainActivity.kt (schritt-03)

MainActivity.kt
package com.example.weatherandroidtif24 import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.ui.Modifier import com.example.weatherandroidtif24.data.fakeCities import com.example.weatherandroidtif24.data.fakeCurrentWeather import com.example.weatherandroidtif24.ui.WeatherScreen import com.example.weatherandroidtif24.ui.theme.WeatherAndroidTIF24Theme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { WeatherAndroidTIF24Theme { val city = fakeCities.first() // Abstand zu Statusleiste und Kamera-Aussparung WeatherScreen( city = city, weather = fakeCurrentWeather.getValue(city.name), modifier = Modifier.safeDrawingPadding(), ) } } } }

Code: schritt-03 · Änderungen 

Scaffold und TopAppBar

Compose bringt die Komponenten von Material Design 3 mit, dem Designsystem von Google. Scaffold ist das Grundgerüst eines Screens, z. B. mit einer TopAppBar.

Scaffold( topBar = { TopAppBar(title = { Text(text = city.name) }) }, ) { innerPadding -> CurrentWeatherCard( city = city, weather = weather, modifier = Modifier.padding(innerPadding), ) }

Das Scaffold steht in WeatherScreen, das dafür @OptIn(ExperimentalMaterial3Api::class) braucht: TopAppBar ist noch als experimentell markiert.

innerPadding ist der Platz, den TopAppBar, Statusleiste und Navigationsleiste belegen. Der Inhalt bekommt ihn als Modifier, damit nichts verdeckt wird. In MainActivity entfällt deshalb Modifier.safeDrawingPadding() mit seinen beiden Imports:

Code: MainActivity.kt (schritt-04)

MainActivity.kt
package com.example.weatherandroidtif24 import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.example.weatherandroidtif24.data.fakeCities import com.example.weatherandroidtif24.data.fakeCurrentWeather import com.example.weatherandroidtif24.ui.WeatherScreen import com.example.weatherandroidtif24.ui.theme.WeatherAndroidTIF24Theme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { WeatherAndroidTIF24Theme { val city = fakeCities.first() WeatherScreen(city = city, weather = fakeCurrentWeather.getValue(city.name)) } } } }

Die Preview legt das Theme der App (WeatherAndroidTIF24Theme) um den Screen. Sonst zeigt sie die Standardfarben von Material 3, nicht die der App:

ui/WeatherScreen.kt
@Preview(showBackground = true) @Composable fun WeatherScreenPreview() { val city = fakeCities.first() WeatherAndroidTIF24Theme { WeatherScreen(city = city, weather = fakeCurrentWeather.getValue(city.name)) } }

Die ganze Datei mit Scaffold, den neuen Imports und der Preview:

Code: ui/WeatherScreen.kt (schritt-04)

ui/WeatherScreen.kt
package com.example.weatherandroidtif24.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.weatherandroidtif24.data.fakeCities import com.example.weatherandroidtif24.data.fakeCurrentWeather import com.example.weatherandroidtif24.model.City import com.example.weatherandroidtif24.model.CurrentWeather import com.example.weatherandroidtif24.ui.theme.WeatherAndroidTIF24Theme import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable fun WeatherScreen(city: City, weather: CurrentWeather, modifier: Modifier = Modifier) { Scaffold( modifier = modifier, topBar = { TopAppBar(title = { Text(text = city.name) }) }, ) { innerPadding -> CurrentWeatherCard( city = city, weather = weather, modifier = Modifier.padding(innerPadding), ) } } @Composable fun CurrentWeatherCard(city: City, weather: CurrentWeather, modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxWidth() .padding(16.dp) .background(Color(0xFFBBDEFB), RoundedCornerShape(24.dp)) .padding(24.dp), contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = city.name, fontSize = 28.sp) Text(text = weather.condition.emoji(), fontSize = 72.sp) Text(text = "${weather.temperature.roundToInt()} °C", fontSize = 48.sp) Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.fillMaxWidth()) { Text(text = "Wind: ${weather.windSpeed.roundToInt()} km/h") Spacer(modifier = Modifier.weight(1f)) Text(text = weather.condition.label()) } } } } @Preview(showBackground = true) @Composable fun WeatherScreenPreview() { val city = fakeCities.first() WeatherAndroidTIF24Theme { WeatherScreen(city = city, weather = fakeCurrentWeather.getValue(city.name)) } }

Die Karte behält vorerst ihre feste Farbe und Schriftgrößen. Farben und Schriften aus dem Theme, Card und Dark Mode kommen am Ende dazu (siehe Material 3).

Code: schritt-04 · Änderungen 

Übung: Karte gestalten

  • Farben, Abstände und Schriftgrößen der Karte ändern, in der Preview ausprobieren
  • Bonus: neuer Parameter highlighted, der die Karte mit einem Rahmen hervorhebt

Der Aufruf in WeatherScreen soll so aussehen:

CurrentWeatherCard( city = city, weather = weather, modifier = Modifier.padding(innerPadding), highlighted = true, )

Ohne highlighted soll die Karte aussehen wie bisher.

Tipps: Ein Parameter mit Standardwert (highlighted: Boolean = false) ist beim Aufruf optional. Einen Rahmen zeichnet Modifier.border(…). Mit .then(…) lässt sich ein Modifier nur unter einer Bedingung in die Kette einfügen; Modifier allein fügt nichts hinzu. Wo in der Kette der Rahmen stehen muss, zeigt der Abschnitt Modifier.

Lösung

ui/WeatherScreen.kt
@Composable fun CurrentWeatherCard( city: City, weather: CurrentWeather, modifier: Modifier = Modifier, highlighted: Boolean = false, ) { val shape = RoundedCornerShape(24.dp) Box( modifier = modifier .fillMaxWidth() .padding(16.dp) .then( if (highlighted) Modifier.border(3.dp, Color(0xFF1565C0), shape) else Modifier, ) .background(Color(0xFFBBDEFB), shape) .padding(24.dp), contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = city.name, fontSize = 28.sp) Text(text = weather.condition.emoji(), fontSize = 72.sp) Text(text = "${weather.temperature.roundToInt()} °C", fontSize = 48.sp) Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.fillMaxWidth()) { Text(text = "Wind: ${weather.windSpeed.roundToInt()} km/h") Spacer(modifier = Modifier.weight(1f)) Text(text = weather.condition.label()) } } } }

Dazu kommt ein Import:

ui/WeatherScreen.kt
import androidx.compose.foundation.border

Der Rahmen steht zwischen den beiden padding: nach dem äußeren Abstand, damit er am Rand der Karte liegt, und vor dem inneren, damit er nicht um den Text läuft. Die Form steht in shape, damit Rahmen und Hintergrund dieselben runden Ecken haben. highlighted steht nach modifier: Nach den Richtlinien von Compose ist modifier der erste optionale Parameter.

Das Referenz-Repository enthält diese Änderung nicht. Die eingeklappten Dateien auf den folgenden Seiten überschreiben sie; wer sie behalten will, muss sie dort selbst wieder einbauen.

Weiterführendes Material