Skip to Content
Mobile Apps5 - React NativeKomponenten-Grundlagen
Folienvorschau
Dauer
20 Minuten
Ziel
Die Bausteine von React Native kennenlernen

Themen

  • Expo Snack und Projekt mit Expo anlegen
  • Komponenten und Fast Refresh
  • View, Text und Flexbox
  • Typen in TypeScript, Fake-Daten
  • Header mit Expo Router
Klicken für Tastaturnavigation · F für Vollbild
Präsentation starten · 20 Minuten ↗

Komponenten-Grundlagen

Wir bauen die Oberfläche der Wetter-App Schritt für Schritt mit React Native auf. Am Ende dieser Seite zeigt die App das Wetter einer Stadt mit Fake-Daten an, genau wie die Android- und die Flutter-App nach Schritt 4.

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. Lange Dateien sind eingeklappt; über jedem Codeblock steht, in welche Datei er gehört. Wo auch die Notizen kürzen, verlinken sie die Datei im Repository mit dem Tag des Schritts.

React Native und Expo

React Native  ist ein Open-Source-Framework von Meta für Apps auf Android und iOS. Programmiert wird in JavaScript oder TypeScript mit React .

  • Komponenten wie View und Text werden zu echten nativen UI-Elementen von Android bzw. iOS
  • dadurch wirken Header, Ladeanzeige oder Schalter wie auf der Plattform gewohnt
  • der JavaScript-Code läuft in einer eigenen Engine (Hermes) auf dem Gerät
  • Expo  ist das Framework rund um React Native: Projektvorlage, Navigation, Werkzeuge, Bibliotheken

Wie Compose und Flutter beschreibt React die Oberfläche deklarativ als Baum verschachtelter Bausteine. Der Unterschied liegt darunter: Flutter zeichnet jedes Pixel selbst, React Native erzeugt die nativen UI-Elemente der Plattform. Eine ScrollView wird auf Android also zu einer nativen ScrollView, auf iOS zu einer UIScrollView. Deshalb sieht eine React-Native-App auf Android und iOS nicht ganz gleich aus, z. B. der Header oder die Ladeanzeige.

Plattformtypisch sehen aber nur die eingebauten Elemente mit eigener Optik aus: Header, ActivityIndicator, Switch, Button, TextInput und Alert-Dialoge. Eine View mit eigenem Style, wie die Wetterkarte auf dieser Seite, sieht auf beiden Plattformen gleich aus. Unter Android sind die nativen Elemente klassische Android-Views wie TextView und ViewGroup, keine Compose-Elemente. Fertige Komponenten im Stil von Material Design bringt React Native nicht mit; dafür gibt es Bibliotheken wie React Native Paper (siehe Theme und Dark Mode).

Die React-Native-Doku  empfiehlt selbst, neue Apps mit einem Framework wie Expo zu beginnen. Wir verwenden Expo und TypeScript , wie in der Vorlage von Expo vorgesehen.

Expo Snack

Expo Snack  ist eine Entwicklungsumgebung für React Native im Browser.

  • Code links, Vorschau rechts: Android, iOS oder Web
  • App auf dem Handy öffnen: QR-Code mit der App Expo Go scannen
  • ideal zum Ausprobieren einzelner Komponenten

Für die Wetter-App arbeiten wir lokal mit VS Code, denn sie besteht aus vielen Dateien und wird mit Git versioniert. Kleine Experimente, z. B. mit Flexbox, gehen in Snack aber schneller.

Snack unterstützt nicht immer sofort die neueste SDK-Version von Expo. Die verwendete Version lässt sich in Snack auswählen.

Projekt anlegen

Voraussetzung: Node.js  (LTS), Git  und VS Code  mit der Extension Expo Tools.

npx create-expo-app@latest WeatherReactNativeTIF24 --template default@sdk-57 cd WeatherReactNativeTIF24 npm run reset-project npx expo lint npx expo start

npx gehört zu Node.js und führt ein Paket aus, ohne es vorher global zu installieren. Fehlt das Paket, lädt npx es herunter. npx expo … verwendet dagegen die Version von Expo, die im Projekt installiert ist.

  • create-expo-app legt ein Projekt mit der Vorlage default an: TypeScript, Expo Router und einige Beispiel-Screens. Ein Git-Repository mit einem ersten Commit legt es gleich mit an.
  • npm run reset-project entfernt die Beispiel-Screens. Die Frage, ob sie nach /example verschoben werden sollen, mit n beantworten. Übrig bleiben src/app/_layout.tsx und src/app/index.tsx.
  • npx expo lint richtet beim ersten Aufruf ESLint ein (Datei eslint.config.js) und prüft dann den Code.
  • npx expo start startet den Entwicklungsserver (Metro). Im Terminal öffnet a die App im Android-Emulator, r lädt sie neu.

--template default@sdk-57 legt die SDK-Version fest. Das ist nötig, weil die App Expo Go aus dem Play Store bzw. App Store immer nur eine SDK-Version öffnen kann (siehe unten). Ohne die Angabe nimmt create-expo-app die neueste Version.

Im Referenz-Repository sind in schritt-00 außerdem das Skript reset-project (Eintrag in package.json und Ordner scripts/) und die nicht mehr benötigten Beispielbilder in assets/images/ gelöscht, z. B. react-logo.png und der Ordner tabIcons/. Die App-Icons und splash-icon.png bleiben. Außerdem gibt es eine eigene AGENTS.md mit Regeln für Coding Agents (siehe Navigation und API).

Wer nicht selbst tippen möchte, klont das Repository, wechselt mit git checkout schritt-00 an den Anfang und führt npm install aus.

App starten: Emulator oder Handy

  • Android-Emulator: Den Emulator aus dem Device Manager von Android Studio starten, dann im Terminal von npx expo start die Taste a drücken. Expo installiert die passende Version von Expo Go im Emulator selbst und öffnet die App.
  • Eigenes Handy: Die App Expo Go aus dem Play Store bzw. App Store installieren und den QR-Code im Terminal scannen (iPhone: mit der Kamera-App). Handy und Rechner müssen im selben WLAN sein. Im WLAN der DHBW klappt das nicht; dann hilft npx expo start --tunnel (Tunneling ).

Expo Go ist eine fertige App, die unseren JavaScript-Code vom Rechner lädt. Einen Build mit Gradle oder Xcode braucht es dafür nicht. Dafür enthält Expo Go nur die nativen Module, die Expo mitliefert; für die Wetter-App reicht das.

Expo Go aus dem Store unterstützt nur die neueste SDK-Version. Stand Oktober 2026 ist das SDK 57; SDK 58 ist gerade erschienen, und Expo Go für SDK 58 folgt in den nächsten Wochen. Danach öffnet Expo Go aus dem Store keine Projekte mit SDK 57 mehr. Im Emulator ist das kein Problem, weil Expo die passende Version selbst installiert. Auf dem eigenen Handy hilft es, automatische Updates für Expo Go abzuschalten, oder das Projekt mit npx expo install expo@^58 --fix auf SDK 58 zu heben.

Ausgangspunkt (schritt-00) ist die Vorlage nach reset-project. src/app/_layout.tsx legt einen Stack an, der die Screens verwaltet und oben einen Header anzeigt:

src/app/_layout.tsx
import { Stack } from "expo-router"; export default function RootLayout() { return <Stack />; }

src/app/index.tsx ist der erste Screen. Dort steht vorerst nur ein Hinweistext in einer View.

Die Datei AGENTS.md aus dem Referenz-Repository steht vollständig auf der Seite Navigation und API. Wer das Projekt selbst anlegt, ersetzt die AGENTS.md der Vorlage spätestens dort.

Projektstruktur

Der Code liegt in src/, nach Aufgaben in Ordner aufgeteilt.

src/ ├── app/ Routen (Screens), _layout.tsx ├── ui/ Komponenten, Hooks (ViewModels), Theme ├── model/ Datenmodell: City, CurrentWeather └── data/ Datenquellen: Fake-Daten, Repositorys └── api/ DTOs für die API (ab Schritt 9) app.json Name, Icon, Einstellungen der App package.json Abhängigkeiten und Skripte

ui/, model/ und data/ sind dieselben Ordner wie in der Android- und der Flutter-App. Neu ist app/: Expo Router macht aus jeder Datei darin einen Screen (dateibasiertes Routing). Was kein Screen ist, gehört nach den Regeln von Expo Router nicht in diesen Ordner.

Pfade im Text dieser und der folgenden Seiten (z. B. ui/weather-screen.tsx) beziehen sich auf src/. Über den Codeblöcken steht der vollständige Pfad ab dem Projektordner. Dateinamen schreiben wir in kebab-case wie die Vorlage von Expo, Komponenten in PascalCase. Importiert wird mit dem Alias @/ für src/, z. B. import type { City } from '@/model/city';. Den Alias richtet die Vorlage in tsconfig.json ein.

package.json entspricht pubspec.yaml bzw. build.gradle.kts: Hier stehen die Abhängigkeiten. Die Ordner android/ und ios/ gibt es nicht. Expo erzeugt sie erst beim Bauen einer eigenen App (Continuous Native Generation ).

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

TypeScript prüft den Code schon beim Tippen. Fehler erscheinen rot unterstrichen und im Panel Problems. Im Terminal prüft npx tsc --noEmit das ganze Projekt.

Komponenten

In React ist alles eine Komponente: eine Funktion, die beschreibt, was angezeigt wird.

Neue Datei ui/weather-screen.tsx im neuen Ordner ui:

src/ui/weather-screen.tsx
import { Text, View } from 'react-native'; export function WeatherScreen() { return ( <View> <Text>Lörrach</Text> <Text>14 °C</Text> </View> ); }

Eine Komponente ist eine Funktion, die JSX zurückgibt: die HTML-ähnlichen Tags im JavaScript-Code. Sie gibt genau ein Element zurück; mehrere Kinder brauchen deshalb eine View, die sie umschließt. Texte müssen in React Native immer in einem Text stehen.

React arbeitet wie Compose und Flutter deklarativ: Wir beschreiben, was angezeigt werden soll. Ändern sich die Daten, ruft React die Funktion erneut auf.

ComposeFlutterReact Native
Funktion mit @ComposableKlasse mit buildFunktion, die JSX zurückgibt
Parameter der FunktionFelder, KonstruktorProps (ein Objekt)
Column { … }Column(children: [ … ])<View>…</View>
Text("…")Text('…')<Text>…</Text>
modifiereigene Widgetsstyle (siehe unten)

Damit WeatherScreen erscheint, ersetzen wir den Inhalt von app/index.tsx:

src/app/index.tsx
import { WeatherScreen } from '@/ui/weather-screen'; export default function Index() { return <WeatherScreen />; }

Eine Route in app/ braucht einen default export; der Name der Funktion ist beliebig. Komponenten in ui/ exportieren wir mit Namen (export function), damit Imports und Umbenennungen in VS Code eindeutig sind.

Über dem Inhalt zeigt der Stack einen Header mit dem Titel „index“, dem Namen der Datei. Das ändern wir in Schritt 4.

Fast Refresh: Speichern genügt, die Änderung ist sofort im Emulator.

  • Fast Refresh (Speichern): Code neu laden, State bleibt erhalten
  • Reload (r im Terminal): App neu laden, State geht verloren
  • neuer Build nötig bei nativen Änderungen, nicht in Expo Go

Fast Refresh entspricht Hot Reload in Flutter. Am besten ausprobieren: Text ändern, speichern, Ergebnis ansehen.

Das Entwicklermenü öffnet sich im Emulator mit ⌘ M bzw. Ctrl+M oder über den runden Knopf mit dem Zahnrad, den Expo Go über die App legt. Verdeckt der Knopf etwas, lässt er sich verschieben.

Fehler im Code zeigt React Native als rote Fehlermeldung direkt in der App an (Redbox). Nach dem Korrigieren und Speichern verschwindet sie.

Code: schritt-01 · Änderungen 

Layouts mit Flexbox

Jede View ist ein Flexbox-Container. Ohne weitere Angabe stehen die Kinder untereinander.

  • flexDirection: 'column' (Standard): untereinander
  • flexDirection: 'row': nebeneinander
  • justifyContent: Verteilung entlang der Hauptachse
  • alignItems: Ausrichtung quer zur Hauptachse
  • flex: 1: nimmt den freien Platz ein

Die Verschachtelung im Code entspricht der Verschachtelung auf dem Bildschirm.

Layout als verschachtelte Views: Bildschirm und Code haben denselben Aufbau

Im Vergleich mit Flutter und Compose fällt auf: Es gibt nur eine Art Box, die View. Hintergrund, Abstände und Anordnung kommen alle aus ihrem style; ein eigenes Container neben Column braucht es nicht. Auch SizedBox und Spacer fehlen: Den Abstand über der Zeile macht marginTop, den freien Platz zwischen den beiden Texten justifyContent: 'space-between' (siehe unten).

src/ui/weather-screen.tsx
function CurrentWeatherCard() { return ( <View style={styles.card}> <Text style={styles.city}>Lörrach</Text> <Text style={styles.emoji}>⛅</Text> <Text style={styles.temperature}>14 °C</Text> <View style={styles.row}> <Text>Wind: 8 km/h</Text> <Text>Teilweise bewölkt</Text> </View> </View> ); }

Die neue Komponente CurrentWeatherCard steht ebenfalls in ui/weather-screen.tsx, ohne export, weil sie nur dort verwendet wird. WeatherScreen gibt nur noch die Karte zurück; der Import ändert sich, weil jetzt auch StyleSheet gebraucht wird:

src/ui/weather-screen.tsx
import { StyleSheet, Text, View } from 'react-native'; export function WeatherScreen() { return <CurrentWeatherCard />; }

Flexbox kennen wir aus CSS. In React Native gibt es zwei Unterschiede: Die Standardrichtung ist column (in CSS row), und jede View ist automatisch ein Flex-Container, ein display: flex braucht es nicht.

Größen sind Zahlen ohne Einheit, gemeint sind dp wie in Android. Text skaliert automatisch mit der Schriftgröße aus den Systemeinstellungen.

Abstände und Hintergrund

Statt Modifier gibt es die Eigenschaft style, gesammelt in einem StyleSheet am Ende der Datei.

Die Styles stehen am Ende von ui/weather-screen.tsx:

src/ui/weather-screen.tsx
const styles = StyleSheet.create({ card: { margin: 16, // außen padding: 24, // innen borderRadius: 24, backgroundColor: '#BBDEFB', alignItems: 'center', }, city: { fontSize: 28 }, emoji: { fontSize: 72 }, temperature: { fontSize: 48 }, row: { flexDirection: 'row', justifyContent: 'space-between', alignSelf: 'stretch', marginTop: 16, }, });

Die Namen kommen aus CSS, geschrieben in camelCase (backgroundColor statt background-color). Wie in Flutter spielt die Reihenfolge keine Rolle: margin ist immer außen, padding immer innen.

alignItems: 'center' zentriert die Texte in der Karte. Die Zeile mit Wind und Wetterlage soll aber die ganze Breite einnehmen: alignSelf: 'stretch' setzt für dieses eine Kind die Ausrichtung des Elternelements außer Kraft. justifyContent: 'space-between' schiebt die beiden Texte an die Ränder, wie Spacer() in Compose und Flutter.

Die Karte ist ohne Angabe so breit wie der Bildschirm, weil Kinder in einer column quer zur Hauptachse gestreckt werden (alignItems: 'stretch' ist der Standard). Ein fillMaxWidth() oder width: double.infinity braucht es nicht.

Am besten mit Fast Refresh ausprobieren: margin, padding oder alignSelf ändern oder entfernen und den Unterschied ansehen.

Code: src/ui/weather-screen.tsx (schritt-02)

src/ui/weather-screen.tsx
import { StyleSheet, Text, View } from 'react-native'; export function WeatherScreen() { return <CurrentWeatherCard />; } function CurrentWeatherCard() { return ( <View style={styles.card}> <Text style={styles.city}>Lörrach</Text> <Text style={styles.emoji}>⛅</Text> <Text style={styles.temperature}>14 °C</Text> <View style={styles.row}> <Text>Wind: 8 km/h</Text> <Text>Teilweise bewölkt</Text> </View> </View> ); } const styles = StyleSheet.create({ card: { margin: 16, // außen padding: 24, // innen borderRadius: 24, backgroundColor: '#BBDEFB', alignItems: 'center', }, city: { fontSize: 28 }, emoji: { fontSize: 72 }, temperature: { fontSize: 48 }, row: { flexDirection: 'row', justifyContent: 'space-between', alignSelf: 'stretch', marginTop: 16, }, });

Code: schritt-02 · Änderungen 

Datenmodell mit Typen

Statt fester Texte bekommen Komponenten nun Daten als Props. Die Form der Daten beschreiben Typen in TypeScript.

src/model/city.ts
export type City = { name: string; latitude: number; longitude: number; };

Der Code oben ist die vollständige Datei model/city.ts im neuen Ordner model. Die Endung ist .ts, weil die Datei kein JSX enthält; Dateien mit Komponenten enden auf .tsx.

Ein type beschreibt nur die Form eines Objekts, wie eine data class ohne Methoden. Objekte dieses Typs schreiben wir als Literal: { name: 'Lörrach', latitude: 47.6156, longitude: 7.6614 }. Fehlt ein Feld oder ist ein Wert vom falschen Typ, meldet TypeScript einen Fehler. Zahlen haben nur einen Typ, number; Int und Double gibt es nicht.

Neue Datei model/current-weather.ts:

src/model/current-weather.ts
export type WeatherCondition = | 'clear' | 'partlyCloudy' | 'cloudy' | 'fog' | 'rain' | 'snow' | 'thunderstorm'; export type CurrentWeather = { temperature: number; // °C windSpeed: number; // km/h condition: WeatherCondition; };

Statt eines enum verwenden wir eine Union aus festen Strings: Eine WeatherCondition ist genau einer dieser sieben Werte. Das ist in TypeScript üblicher als enum, und der Wert ist zur Laufzeit ein normaler String.

Die Fake-Daten liegen in data/fake-data.ts im neuen Ordner data:

src/data/fake-data.ts
import type { City } from '@/model/city'; import type { CurrentWeather } from '@/model/current-weather'; export const fakeCities: City[] = [ { name: 'Lörrach', latitude: 47.6156, longitude: 7.6614 }, { name: 'Freiburg', latitude: 47.999, longitude: 7.8421 }, { name: 'Basel', latitude: 47.5596, longitude: 7.5886 }, ]; export const fakeCurrentWeather: Record<string, CurrentWeather> = { Lörrach: { temperature: 14.2, windSpeed: 8.5, condition: 'partlyCloudy' }, Freiburg: { temperature: 15.0, windSpeed: 6.1, condition: 'clear' }, Basel: { temperature: 13.6, windSpeed: 11.2, condition: 'rain' }, };

City[] ist eine Liste von City. Record<string, CurrentWeather> ist ein Objekt mit beliebigen String-Schlüsseln und Werten vom Typ CurrentWeather, wie eine Map. import type importiert nur einen Typ; im fertigen JavaScript bleibt davon nichts übrig.

TypeScript für Kotlin- und Dart-Kenner: Der Typ steht hinter dem Namen wie in Kotlin (name: string). const entspricht val bzw. final, let entspricht var. Strings stehen in '…', Templates in Backticks: `${city.name}`. Null-Safety gibt es mit string | null, ?. und ??. Die Typen prüft nur der Compiler; zur Laufzeit ist alles normales JavaScript.

Komponenten sollten Daten als Props erhalten und sie nicht eigenständig holen.

src/ui/weather-screen.tsx
type CurrentWeatherCardProps = { city: City; weather: CurrentWeather; }; function CurrentWeatherCard({ city, weather }: CurrentWeatherCardProps) { return ( <View style={styles.card}> <Text style={styles.city}>{city.name}</Text> <Text style={styles.emoji}>{conditionEmoji[weather.condition]}</Text> <Text style={styles.temperature}> {Math.round(weather.temperature)} °C </Text> <View style={styles.row}> <Text>Wind: {Math.round(weather.windSpeed)} km/h</Text> <Text>{conditionLabel[weather.condition]}</Text> </View> </View> ); }

Eine Komponente bekommt ein Objekt mit allen Props. Ohne Destructuring sähe der Anfang so aus:

function CurrentWeatherCard(props: CurrentWeatherCardProps) { return ( <View style={styles.card}> <Text style={styles.city}>{props.city.name}</Text>

Beim Aufruf <CurrentWeatherCard city={city} weather={weather} /> ist props das Objekt { city: …, weather: … }; die Attribute werden zu seinen Feldern. Weil props. überall stört, holen wir die Felder mit { city, weather } gleich in der Parameterliste heraus (Destructuring). Das ist kürzer und in React üblich. Der Typ CurrentWeatherCardProps legt fest, welche Props es gibt. Fehlt beim Aufruf eine, meldet TypeScript einen Fehler.

In JSX steht JavaScript in geschweiften Klammern: {city.name}, {Math.round(weather.temperature)}.

Die Anzeige der Wetterlage steht in ui/weather-condition-ui.ts. Sie gehört zur UI, nicht ins Datenmodell:

src/ui/weather-condition-ui.ts
import type { WeatherCondition } from '@/model/current-weather'; export const conditionEmoji: Record<WeatherCondition, string> = { clear: '☀️', partlyCloudy: '⛅', cloudy: '☁️', fog: '🌫️', rain: '🌧️', snow: '❄️', thunderstorm: '⛈️', }; export const conditionLabel: Record<WeatherCondition, string> = { clear: 'Sonnig', partlyCloudy: 'Teilweise bewölkt', cloudy: 'Bewölkt', fog: 'Nebel', rain: 'Regen', snow: 'Schnee', thunderstorm: 'Gewitter', };

Record<WeatherCondition, string> verlangt für jeden Wert der Union einen Eintrag. Kommt eine Wetterlage hinzu, meldet TypeScript jede Tabelle, in der sie fehlt, wie when in Kotlin oder switch in Dart.

WeatherScreen bekommt city und weather als Props und reicht sie an die Karte weiter. Der Anfang von ui/weather-screen.tsx:

src/ui/weather-screen.tsx
import { StyleSheet, Text, View } from 'react-native'; import type { City } from '@/model/city'; import type { CurrentWeather } from '@/model/current-weather'; import { conditionEmoji, conditionLabel } from '@/ui/weather-condition-ui'; type WeatherScreenProps = { city: City; weather: CurrentWeather; }; export function WeatherScreen({ city, weather }: WeatherScreenProps) { return <CurrentWeatherCard city={city} weather={weather} />; }

Darunter folgen CurrentWeatherCard von oben und styles wie bisher:

Code: src/ui/weather-screen.tsx (schritt-03)

src/ui/weather-screen.tsx
import { StyleSheet, Text, View } from 'react-native'; import type { City } from '@/model/city'; import type { CurrentWeather } from '@/model/current-weather'; import { conditionEmoji, conditionLabel } from '@/ui/weather-condition-ui'; type WeatherScreenProps = { city: City; weather: CurrentWeather; }; export function WeatherScreen({ city, weather }: WeatherScreenProps) { return <CurrentWeatherCard city={city} weather={weather} />; } type CurrentWeatherCardProps = { city: City; weather: CurrentWeather; }; function CurrentWeatherCard({ city, weather }: CurrentWeatherCardProps) { return ( <View style={styles.card}> <Text style={styles.city}>{city.name}</Text> <Text style={styles.emoji}>{conditionEmoji[weather.condition]}</Text> <Text style={styles.temperature}> {Math.round(weather.temperature)} °C </Text> <View style={styles.row}> <Text>Wind: {Math.round(weather.windSpeed)} km/h</Text> <Text>{conditionLabel[weather.condition]}</Text> </View> </View> ); } const styles = StyleSheet.create({ card: { margin: 16, // außen padding: 24, // innen borderRadius: 24, backgroundColor: '#BBDEFB', alignItems: 'center', }, city: { fontSize: 28 }, emoji: { fontSize: 72 }, temperature: { fontSize: 48 }, row: { flexDirection: 'row', justifyContent: 'space-between', alignSelf: 'stretch', marginTop: 16, }, });

app/index.tsx übergibt die Fake-Daten:

src/app/index.tsx
import { fakeCities, fakeCurrentWeather } from '@/data/fake-data'; import { WeatherScreen } from '@/ui/weather-screen'; export default function Index() { const city = fakeCities[0]; return <WeatherScreen city={city} weather={fakeCurrentWeather[city.name]} />; }

Anders als Dart meldet TypeScript hier nichts, obwohl es den Schlüssel vielleicht nicht gibt: Für ein Record<string, …> nimmt es an, dass jeder Schlüssel existiert. Fehlt er doch, ist der Wert undefined, und die App stürzt beim Zugriff auf weather.condition ab.

Code: schritt-03 · Änderungen 

Header mit Expo Router

Den Header zeigt der Stack aus dem Layout. Jeder Screen setzt seinen Titel selbst.

src/ui/weather-screen.tsx
export function WeatherScreen({ city, weather }: WeatherScreenProps) { return ( <ScrollView> <Stack.Screen options={{ title: city.name }} /> <CurrentWeatherCard city={city} weather={weather} /> </ScrollView> ); }

Dazu kommen zwei Imports am Anfang von ui/weather-screen.tsx:

src/ui/weather-screen.tsx
import { Stack } from 'expo-router'; import { ScrollView, StyleSheet, Text, View } from 'react-native';

<Stack.Screen> zeigt selbst nichts an. Es setzt nur die Optionen des Screens, in dem es steht, hier den Titel. Weitere Optionen folgen mit dem Umschalter °C/°F. Das entspricht Scaffold mit TopAppBar bzw. AppBar, nur dass das Gerüst im Layout steht und jeder Screen es anpasst.

ScrollView macht den Inhalt scrollbar, wenn er nicht auf den Bildschirm passt. Anders als im Web scrollt in React Native nichts von selbst. Zu große Inhalte werden abgeschnitten. Die App ist in app.json auf Hochformat festgelegt ("orientation": "portrait"); Querformat spielt hier also keine Rolle.

Der Header berücksichtigt die Statusleiste selbst. Ein SafeAreaView braucht es dafür nicht. Ohne Header, z. B. bei einem Vollbild-Screen, übernimmt das die Bibliothek react-native-safe-area-context , die Expo schon mitbringt.

Die Karte behält vorerst ihre feste Farbe und Schriftgrößen. Farben aus einem Theme und Dark Mode kommen am Ende dazu (siehe Theme und Dark Mode).

Code: src/ui/weather-screen.tsx (schritt-04)

src/ui/weather-screen.tsx
import { Stack } from 'expo-router'; import { ScrollView, StyleSheet, Text, View } from 'react-native'; import type { City } from '@/model/city'; import type { CurrentWeather } from '@/model/current-weather'; import { conditionEmoji, conditionLabel } from '@/ui/weather-condition-ui'; type WeatherScreenProps = { city: City; weather: CurrentWeather; }; export function WeatherScreen({ city, weather }: WeatherScreenProps) { return ( <ScrollView> <Stack.Screen options={{ title: city.name }} /> <CurrentWeatherCard city={city} weather={weather} /> </ScrollView> ); } type CurrentWeatherCardProps = { city: City; weather: CurrentWeather; }; function CurrentWeatherCard({ city, weather }: CurrentWeatherCardProps) { return ( <View style={styles.card}> <Text style={styles.city}>{city.name}</Text> <Text style={styles.emoji}>{conditionEmoji[weather.condition]}</Text> <Text style={styles.temperature}> {Math.round(weather.temperature)} °C </Text> <View style={styles.row}> <Text>Wind: {Math.round(weather.windSpeed)} km/h</Text> <Text>{conditionLabel[weather.condition]}</Text> </View> </View> ); } const styles = StyleSheet.create({ card: { margin: 16, // außen padding: 24, // innen borderRadius: 24, backgroundColor: '#BBDEFB', alignItems: 'center', }, city: { fontSize: 28 }, emoji: { fontSize: 72 }, temperature: { fontSize: 48 }, row: { flexDirection: 'row', justifyContent: 'space-between', alignSelf: 'stretch', marginTop: 16, }, });

Code: schritt-04 · Änderungen 

Übung: Karte gestalten

  • Farben, Abstände und Schriftgrößen der Karte ändern, mit Fast Refresh ausprobieren
  • Bonus: neue Prop highlighted, die die Karte mit einem Rahmen hervorhebt

Der Aufruf soll so aussehen:

<CurrentWeatherCard city={city} weather={weather} highlighted />

Eine Prop ohne Wert wie highlighted ist die Kurzform für highlighted={true}. Ohne die Prop soll die Karte aussehen wie bisher.

Tipps: Eine optionale Prop bekommt im Typ ein ? (highlighted?: boolean) und in der Parameterliste einen Standardwert. Ein Rahmen entsteht mit borderWidth und borderColor. style nimmt auch ein Array von Styles.

Lösung

src/ui/weather-screen.tsx
type CurrentWeatherCardProps = { city: City; weather: CurrentWeather; highlighted?: boolean; }; function CurrentWeatherCard({ city, weather, highlighted = false, }: CurrentWeatherCardProps) { return ( <View style={[styles.card, highlighted && styles.highlighted]}> <Text style={styles.city}>{city.name}</Text> <Text style={styles.emoji}>{conditionEmoji[weather.condition]}</Text> <Text style={styles.temperature}> {Math.round(weather.temperature)} °C </Text> <View style={styles.row}> <Text>Wind: {Math.round(weather.windSpeed)} km/h</Text> <Text>{conditionLabel[weather.condition]}</Text> </View> </View> ); }

In styles kommt ein Eintrag hinzu:

src/ui/weather-screen.tsx
highlighted: { borderWidth: 3, borderColor: '#1565C0' },

highlighted && styles.highlighted ergibt false, wenn die Prop fehlt; false ignoriert React Native in einem Style-Array. Bei gleichen Eigenschaften gewinnt der letzte Style im Array.

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