Widget-Grundlagen
Wir bauen die Oberfläche der Wetter-App Schritt für Schritt mit Flutter auf. Am Ende dieser Seite zeigt die App das Wetter einer Stadt mit Fake-Daten an, genau wie die Android-App nach Schritt 4.
Code: Repository auf GitHub , Schritte 0 bis 4.
Codebeispiele mit // ... sind gekürzt; der vollständige Code steht im
Repository unter dem jeweiligen Tag.
Flutter und Dart
Flutter ist ein Open-Source-Framework von Google für Apps auf Android, iOS, Web und Desktop aus einer Codebasis. Programmiert wird in Dart .
- Flutter zeichnet die Oberfläche selbst, statt native UI-Elemente zu verwenden
- dadurch sieht die App auf allen Plattformen gleich aus
- Dart wird für Android und iOS in nativen Maschinencode kompiliert
Compose und Flutter verfolgen dasselbe Prinzip: Die Oberfläche wird deklarativ als Baum verschachtelter Bausteine beschrieben. Der Unterschied liegt darunter: Compose erzeugt eine Android-App, Flutter bringt eine eigene Rendering-Engine mit und zeichnet jedes Pixel selbst, auf Android, iOS und im Browser.
Im Oktober 2024 entstand mit Flock ein Community-Fork von Flutter, angestoßen von Matt Carroll, einem ehemaligen Mitglied des Flutter-Teams (Hintergrund ). Flock sollte Flutter ergänzen und Fixes schneller ausliefern, nicht ersetzen.
Stand Oktober 2026: Im Flock-Repository gibt es seit Dezember 2025 keine neuen Commits, Releases gab es nie. Für die Vorlesung und das Projekt verwenden wir das Original von Google.
Projekt in VS Code anlegen
Die Flutter-Dokumentation empfiehlt VS Code mit der Flutter-Extension.
- Extension Flutter installieren (bringt Dart mit)
- Command Palette → Flutter: New Project → Empty Application
- Projektname:
weather_flutter_tif24 - Gerät unten rechts in der Statusleiste wählen, starten mit F5
Fehlt das Flutter-SDK, bietet die Extension an, es herunterzuladen
(Download SDK). Als Ordner eignet sich ~/develop bzw.
C:\Users\<Name>\develop: nicht der Projektordner, kein Cloud-Ordner wie
OneDrive, keine Leerzeichen oder Umlaute im Pfad (sonst z. B.
C:\develop). Danach Add SDK to PATH bestätigen und VS Code neu
starten. Wer einen Paketmanager nutzt, kann Flutter auch darüber
installieren, z. B. mit Homebrew unter macOS
(brew install --cask flutter), Chocolatey unter Windows
(choco install flutter) oder Snap unter Ubuntu
(sudo snap install flutter --classic); aktualisiert wird dann
ebenfalls über den Paketmanager. flutter doctor im Terminal zeigt, ob
alles eingerichtet ist.
Den Android-Emulator kennen wir schon: Er kommt aus dem Device Manager von Android Studio. Android Studio muss dafür installiert sein, aber nicht laufen. In VS Code erscheint der Emulator in der Geräteauswahl (auch über Flutter: Select Device). Ohne Emulator läuft die App auch in Chrome oder als macOS-App.
Projektnamen in Dart bestehen aus Kleinbuchstaben und Unterstrichen.
Das Referenz-Repository heißt WeatherFlutterTIF24, das Projekt darin
weather_flutter_tif24. Ein Git-Repository legt Flutter nicht an, das
geht mit git init im Projektordner.
Android Studio mit dem Flutter-Plugin funktioniert genauso. Wer dort schon zu Hause ist, kann dabei bleiben. In der Vorlesung verwenden wir VS Code, um zu zeigen, dass Flutter an keine IDE gebunden ist.
Ausgangspunkt (schritt-00) ist die Vorlage Empty Application. In
lib/main.dart bleibt nur das Nötigste übrig:
void main() {
runApp(const WeatherApp());
}
class WeatherApp extends StatelessWidget {
const WeatherApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Wetter',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: const Scaffold(body: SafeArea(child: Placeholder())),
);
}
}runApp startet die App mit dem obersten Widget. MaterialApp richtet
Theme und Navigation ein, home ist der erste Screen. Placeholder
zeichnet ein durchgestrichenes Rechteck, bis dort echter Inhalt steht.
Zusätzlich liegt im Repository eine Datei AGENTS.md mit Regeln für
Coding Agents (siehe Navigation und API).
Projektstruktur
Der Dart-Code liegt in lib/, nach Aufgaben in Ordner aufgeteilt.
lib/
├── main.dart
├── ui/ Screens und Widgets, ViewModels
├── model/ Datenmodell: City, CurrentWeather
└── data/ Datenquellen: Fake-Daten, Repositorys
└── api/ DTOs für die API (ab Schritt 9)
pubspec.yaml Name, Abhängigkeiten, Assets
android/ ios/ web/ Plattform-Projekte, selten angefasstDie Ordner sind dieselben wie in der Android-App. Pfade auf dieser und
den folgenden Seiten (z. B. ui/weather_screen.dart) beziehen sich auf
lib/. Dateinamen schreibt man in Dart in snake_case, Klassennamen in
PascalCase. Importiert wird mit relativen Pfaden, z. B.
import '../model/city.dart';.
pubspec.yaml entspricht build.gradle.kts und
libs.versions.toml zusammen: Hier stehen die Abhängigkeiten (Pakete
von pub.dev ).
Die Ordner android/ und ios/ enthalten echte native Projekte, die
Flutter beim Bauen verwendet. Angepasst werden sie nur für Dinge wie
Berechtigungen oder App-Icons.
Tipps für VS Code
| Aktion | macOS | Windows/Linux |
|---|---|---|
| Quick Fix, z. B. fehlender Import | ⌘ . | Ctrl+. |
| Code formatieren (Format Document) | ⇧ ⌥ F | Shift+Alt+F |
| Umbenennen, überall (Rename Symbol) | F2 | F2 |
| Zur Definition springen | ⌘ + Klick | Ctrl+Klick |
| Parameter einer Funktion anzeigen | ⇧ ⌘ Leertaste | Ctrl+Shift+Leertaste |
| Command Palette | ⇧ ⌘ P | Ctrl+Shift+P |
Besonders praktisch: Quick Fix auf einem Widget bietet Wrap with Column, Wrap with Padding, Remove this widget usw. an. Damit lässt sich der Widget-Baum umbauen, ohne Klammern zu zählen.
Snippets sparen Tipparbeit: stless + Tab erzeugt ein
StatelessWidget, stful + Tab ein StatefulWidget.
Formatieren beim Speichern: Settings → „Format On Save“ aktivieren.
Widgets
In Flutter ist alles ein Widget: Text, Abstände, Layouts, ganze Screens.
Neue Datei ui/weather_screen.dart:
class WeatherScreen extends StatelessWidget {
const WeatherScreen({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
Text('Lörrach'),
Text('14 °C'),
],
);
}
}Ein Widget ist eine Klasse. build beschreibt, wie es aussieht, und
gibt genau ein Widget zurück. Mehrere Kinder brauchen deshalb ein Layout
wie Column; das Problem mit überlappenden Texten aus der Android-App
gibt es so nicht.
Flutter arbeitet wie Compose deklarativ: Wir beschreiben, was
angezeigt werden soll. Ändern sich die Daten, ruft Flutter build erneut
auf.
| Compose | Flutter |
|---|---|
Funktion mit @Composable | Klasse, die von StatelessWidget erbt |
| Parameter der Funktion | Felder der Klasse, Konstruktor |
| Inhalt im Funktionsrumpf | Rückgabewert von build |
Column { … } (Trailing Lambda) | Column(children: [ … ]) |
modifier | eigene Widgets und Eigenschaften (siehe unten) |
Konventionen für Widgets:
const-Konstruktor mit{super.key}:keybrauchen wir erst bei Listen, er gehört aber zur Konvention- Felder sind
final, ein Widget ist unveränderlich constvor einem Widget sagt Flutter, dass es sich nie ändert; es wird nur einmal erzeugt
Damit WeatherScreen erscheint, setzt main.dart es statt des
Placeholder ein:
home: const Scaffold(body: SafeArea(child: WeatherScreen())),SafeArea hält den Inhalt aus Statusleiste und Kamera-Aussparung heraus,
wie safeDrawingPadding() in Compose.
Hot Reload: Speichern genügt, die Änderung ist in unter einer Sekunde im laufenden Emulator.
- Hot Reload (Speichern): Code neu laden, State bleibt erhalten
- Hot Restart (⇧ ⌘ F5): App neu starten, State geht verloren
- neuer Build nötig bei Änderungen an
pubspec.yamloder nativem Code
Hot Reload übernimmt die Rolle der @Preview in Android Studio, nur
direkt im Emulator und mit echter Interaktion. Am besten ausprobieren:
Text ändern, speichern, Ergebnis ansehen.
Manche Änderungen kommen mit Hot Reload nicht an, z. B. neue
Startwerte von State oder Änderungen in main(). Dann hilft
Hot Restart.
Seit Flutter 3.35 gibt es auch
Widget Previews ähnlich
wie @Preview in Compose. Sie sind noch experimentell; in dieser Lektion
verwenden wir Hot Reload.
Code: schritt-01 ·
Änderungen
Layouts: Column, Row, Container
Widgets werden mit Layout-Widgets angeordnet.
Column: untereinanderRow: nebeneinanderStack: übereinanderContainer: Hintergrund, Rahmen, Abstände um ein KindSizedBox: fester Abstand,Spacer: freier Platz
Die Verschachtelung im Code entspricht der Verschachtelung auf dem Bildschirm.
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Lörrach', style: TextStyle(fontSize: 28)),
Text('⛅', style: TextStyle(fontSize: 72)),
Text('14 °C', style: TextStyle(fontSize: 48)),
SizedBox(height: 16),
Row(children: [Text('Wind: 8 km/h'), Spacer(), Text('Teilweise bewölkt')]),
],
)Dieser Code steckt in einem neuen Widget CurrentWeatherCard, ebenfalls
in ui/weather_screen.dart. WeatherScreen gibt nur noch
const CurrentWeatherCard() zurück.
Column und Row zentrieren ihre Kinder quer zur Hauptachse
standardmäßig. Entlang der Hauptachse nimmt eine Column so viel Platz
ein wie möglich; mainAxisSize: MainAxisSize.min macht sie so klein wie
ihr Inhalt.
Spacer() nimmt den gesamten freien Platz in der Row ein und schiebt
so den zweiten Text an den rechten Rand, wie Spacer(Modifier.weight(1f))
in Compose.
Größen sind logische Pixel, das entspricht dp. Eine eigene Einheit
für Schrift wie sp gibt es nicht: Flutter skaliert Text automatisch mit
der Schriftgröße aus den Systemeinstellungen.
Im Repository steht die Row über mehrere Zeilen, so wie sie der
Formatierer (dart format) anordnet. Seit Dart 3.7 entscheidet er
selbst nach der Zeilenlänge, ob ein Aufruf in eine oder mehrere Zeilen
kommt, und setzt oder entfernt die abschließenden Kommas passend.
Speichern genügt, von Hand formatieren muss man nichts.
Abstände und Hintergrund
Einen Modifier gibt es nicht. Abstände und Hintergrund sind Eigenschaften von Container oder eigene Widgets wie Padding.
Container(
width: double.infinity,
margin: const EdgeInsets.all(16), // außen
padding: const EdgeInsets.all(24), // innen
decoration: BoxDecoration(
color: const Color(0xFFBBDEFB),
borderRadius: BorderRadius.circular(24),
),
child: const Column(/* von oben */),
)Anders als bei Modifiern in Compose spielt die Reihenfolge der
Argumente keine Rolle: margin ist immer außen, padding immer innen.
Dafür wird der Baum tiefer, sobald mehrere Widgets ineinander stecken,
z. B. Padding(padding: …, child: Center(child: …)).
width: double.infinity entspricht fillMaxWidth(): so breit wie der
verfügbare Platz.
Am besten mit Hot Reload ausprobieren: margin oder padding ändern
oder entfernen und den Unterschied ansehen.
Code: schritt-02 ·
Änderungen
Datenmodell mit Klassen
Statt fester Texte bekommen Widgets nun Daten über den Konstruktor.
class City {
const City({
required this.name,
required this.latitude,
required this.longitude,
});
final String name;
final double latitude;
final double longitude;
}Neue Dateien model/city.dart und model/current_weather.dart im neuen
Ordner model.
Eine data class wie in Kotlin gibt es in Dart nicht. Wir schreiben
eine normale Klasse mit final-Feldern und einem const-Konstruktor.
{ … } macht die Parameter zu benannten Parametern, required
macht sie zu Pflichtangaben. Aufgerufen wird mit Namen, wie in Kotlin:
City(name: 'Lörrach', latitude: 47.6156, longitude: 7.6614).
equals und copy erzeugt Dart nicht automatisch; wir brauchen sie hier
auch nicht.
CurrentWeather ist genauso aufgebaut (temperature, windSpeed,
condition). Für die Wetterlage gibt es ein enum, Werte in
lowerCamelCase:
enum WeatherCondition { clear, partlyCloudy, cloudy, fog, rain, snow, thunderstorm }Die Koordinaten in City brauchen wir erst in Schritt 9 für die API.
Die Fake-Daten liegen in data/fake_data.dart im neuen Ordner data:
const fakeCities = [
City(name: 'Lörrach', latitude: 47.6156, longitude: 7.6614),
// ... Freiburg, Basel
];
const fakeCurrentWeather = {
'Lörrach': CurrentWeather(
temperature: 14.2,
windSpeed: 8.5,
condition: WeatherCondition.partlyCloudy,
),
// ... Freiburg, Basel
};Listen ([…]) und Maps ({…}) haben in Dart eigene Literale. Die Typen
List<City> und Map<String, CurrentWeather> leitet Dart ab.
Vollständig:
fake_data.dart
Dart für Kotlin-Kenner: Vieles ist ähnlich, z. B. Null-Safety mit
String?, ?. und !, Extension Methods oder String-Templates mit
'$name' bzw. '${city.name}'. Unterschiede: Semikolons sind Pflicht,
der Typ steht vor dem Namen (final String name), Strings meist in
einfachen Anführungszeichen, und _ am Namensanfang macht etwas
privat (statt private).
Widgets sollten Daten über den Konstruktor erhalten und sie nicht eigenständig holen.
class CurrentWeatherCard extends StatelessWidget {
const CurrentWeatherCard({super.key, required this.city, required this.weather});
final City city;
final CurrentWeather weather;
@override
Widget build(BuildContext context) {
// Container und Column wie bisher, die Texte jetzt aus den Daten:
// Text(city.name), Text(weather.condition.emoji), ...
}
}Weil die Texte jetzt aus Variablen kommen, entfällt das const vor der
Column; nur die festen TextStyles bleiben const.
Die Anzeige der Wetterlage ist eine Extension mit einem
switch-Ausdruck in ui/weather_condition_ui.dart. Sie gehört zur UI,
nicht ins Datenmodell:
extension WeatherConditionUi on WeatherCondition {
String get emoji => switch (this) {
WeatherCondition.clear => '☀️',
WeatherCondition.partlyCloudy => '⛅',
// ...
};
}Wie when in Kotlin muss der switch-Ausdruck alle Werte des enum
abdecken. Kommt ein neuer Wert hinzu, meldet der Analyzer jede Stelle,
die angepasst werden muss. emoji ist ein Getter (get), deshalb steht
beim Aufruf keine Klammer: weather.condition.emoji. Nach demselben
Muster liefert label den Text („Sonnig“, „Regen“ …).
Auch WeatherScreen bekommt city und weather über den Konstruktor
und reicht sie an die Karte weiter. main.dart übergibt die Fake-Daten:
WeatherScreen(
city: fakeCities.first,
weather: fakeCurrentWeather[fakeCities.first.name]!,
)Der Zugriff auf eine Map liefert CurrentWeather?, weil es den
Schlüssel vielleicht nicht gibt. ! sagt: Hier ist der Wert sicher da.
Fehlt er doch, stürzt die App ab, wie mit getValue in Kotlin.
Code: schritt-03 ·
Änderungen
Scaffold und AppBar
Flutter bringt die Widgets von Material Design 3 mit. Scaffold ist das Grundgerüst eines Screens, z. B. mit einer AppBar.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(city.name)),
body: SingleChildScrollView(
child: CurrentWeatherCard(city: city, weather: weather),
),
);
}Das Scaffold wandert aus main.dart in WeatherScreen. main.dart
setzt WeatherScreen jetzt direkt als home ein, ohne Scaffold und
SafeArea:
home: WeatherScreen(
city: fakeCities.first,
weather: fakeCurrentWeather[fakeCities.first.name]!,
),Ein innerPadding wie in Compose gibt es nicht: Das Scaffold legt
body automatisch unter die AppBar, und die AppBar berücksichtigt die
Statusleiste selbst. Für den oberen Rand ist SafeArea deshalb nicht
mehr nötig. Seitliche Aussparungen im Querformat schützt die AppBar
nicht; bei Inhalten bis an den Rand hilft dort weiterhin SafeArea.
SingleChildScrollView macht den Inhalt scrollbar, wenn er nicht auf
den Bildschirm passt. Im Querformat ist die Karte höher als der freie
Platz; ohne Scrollen zeigt Flutter einen gelb-schwarz gestreiften
Balken (Overflow). Anders als in Compose scrollt in Flutter nichts von
selbst, und zu große Inhalte werden nicht stillschweigend
abgeschnitten.
Die AppBar folgt den Konventionen der Plattform: Auf Android steht
der Titel links, auf iOS mittig.
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
Weiterführendes Material
- Flutter einrichten
- Flutter für Android-Entwickler : Vergleich mit Compose und Views
- Layouts in Flutter
- Widget-Katalog
- Hot Reload
- Dart-Sprachtour