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:
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
| Aktion | macOS | Windows/Linux |
|---|---|---|
| Quick Fix, z. B. fehlender Import | ⌥ ⏎ | Alt+Enter |
| Imports aufräumen (Optimize Imports) | ⌃ ⌥ O | Ctrl+Alt+O |
| Code formatieren (Reformat Code) | ⌥ ⌘ L | Ctrl+Alt+L |
| Umbenennen, überall (Rename) | ⇧ F6 | Shift+F6 |
| Zur Definition springen | ⌘ + Klick | Ctrl+Klick |
| Parameter einer Funktion anzeigen | ⌘ P | Ctrl+P |
| Alles durchsuchen | 2× ⇧ | 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:
| Framework | Baustein | Beispiel |
|---|---|---|
| Compose | Composable | Column { Text("Lörrach") } |
| Flutter | Widget | Column(children: [Text('Lörrach')]) |
| React Native | Komponente | <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 leereModifier; 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 (schritt-01)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 (schritt-01)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: untereinanderRow: nebeneinanderBox: übereinander, z. B. Inhalt auf einem HintergrundSpacer: Abstand bzw. freier Platz
Die Verschachtelung im Code entspricht der Verschachtelung auf dem Bildschirm.
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:
@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 (schritt-02)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:
package com.example.weatherandroidtif24.model
data class City(
val name: String,
val latitude: Double,
val longitude: Double,
)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.
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:
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 (schritt-03)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 (schritt-03)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 (schritt-04)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:
@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 (schritt-04)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
@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:
import androidx.compose.foundation.borderDer 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
- UI-Entwicklung für Android : Einstieg mit Tutorials, Beispielen und Quick Guides
- Thinking in Compose
- Compose-Layouts: Grundlagen
- Previews
- Modifier und Liste aller Modifier