Skip to Content
Folienvorschau
Dauer
20 Minuten
Ziel
Die Bausteine von Flutter kennenlernen

Themen

  • Projekt in VS Code anlegen
  • Widgets und Hot Reload
  • Column, Row, Container und SizedBox
  • Klassen und enum in Dart, Fake-Daten
  • Scaffold und AppBar
Klicken für Tastaturnavigation · F für Vollbild
Präsentation starten · 20 Minuten ↗

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 angefasst

Die 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

AktionmacOSWindows/Linux
Quick Fix, z. B. fehlender Import⌘ .Ctrl+.
Code formatieren (Format Document)⇧ ⌥ FShift+Alt+F
Umbenennen, überall (Rename Symbol)F2F2
Zur Definition springen⌘ + KlickCtrl+Klick
Parameter einer Funktion anzeigen⇧ ⌘ LeertasteCtrl+Shift+Leertaste
Command Palette⇧ ⌘ PCtrl+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.

ComposeFlutter
Funktion mit @ComposableKlasse, die von StatelessWidget erbt
Parameter der FunktionFelder der Klasse, Konstruktor
Inhalt im FunktionsrumpfRückgabewert von build
Column { … } (Trailing Lambda)Column(children: [ … ])
modifiereigene Widgets und Eigenschaften (siehe unten)

Konventionen für Widgets:

  • const-Konstruktor mit {super.key}: key brauchen wir erst bei Listen, er gehört aber zur Konvention
  • Felder sind final, ein Widget ist unveränderlich
  • const vor 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.yaml oder 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: untereinander
  • Row: nebeneinander
  • Stack: übereinander
  • Container: Hintergrund, Rahmen, Abstände um ein Kind
  • SizedBox: fester Abstand, Spacer: freier Platz

Die Verschachtelung im Code entspricht der Verschachtelung auf dem Bildschirm.

Layout als verschachtelte Boxen: Bildschirm und Code haben denselben Aufbau

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