📱 Was du in diesem Guide lernst
Dieser Guide zeigt, wie Claude Code mit Expo und React Native zusammenarbeitet, um professionelle Mobile Apps für iOS und Android zu entwickeln — von Null bis App Store.
- create-expo-app
- Expo Router
- NativeWind
- expo-camera
- EAS Build
- OTA Updates
- Push Notifications
1. Expo Setup — create-expo-app, app.json und Development Build
Expo ist das führende Framework für React Native Apps. Mit create-expo-app startet Claude Code ein neues Projekt in Sekunden — mit sofort funktionierender iOS- und Android-Unterstützung, ohne native Build-Tools manuell einzurichten.
Neues Projekt anlegen
# Neues Expo-Projekt mit TypeScript-Template
npx create-expo-app@latest MyApp --template blank-typescript
cd MyApp
# Abhängigkeiten installieren
npm install
# Expo Go starten (schnelles Prototyping)
npx expo start
Expo Go ermöglicht sofortiges Testen auf dem eigenen Gerät via QR-Code — ideal für die Entwicklung ohne Build-Schritt. Sobald native Module benötigt werden, wechselt Claude Code auf einen Development Build.
app.json — Projekt-Konfiguration
// app.json — Expo-Projekt-Konfiguration
{
"expo": {
"name": "MyApp",
"slug": "myapp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#0a0a0f"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.mycompany.myapp",
"buildNumber": "1"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#0a0a0f"
},
"package": "com.mycompany.myapp",
"versionCode": 1
},
"plugins": [
"expo-router",
"expo-notifications",
["expo-camera", { "cameraPermission": "Zugriff auf Kamera" }]
],
"experiments": {
"typedRoutes": true
}
}
}
app.config.ts statt app.json: Für dynamische Konfiguration — z. B. unterschiedliche Bundle-IDs per Umgebungsvariable — empfiehlt Claude Code app.config.ts, das JavaScript-Logik erlaubt und zur Build-Zeit ausgeführt wird.
Expo Go vs Development Build
| Modus | Vorteile | Einschränkungen |
|---|---|---|
| Expo Go | Sofort einsatzbereit, kein Build nötig, QR-Code-Scan | Keine custom native Modules, kein vollständiges API-Set |
| Development Build | Alle native Module, vollständige Expo-API, Debug-Tools | EAS Build oder lokale Xcode/Android Studio nötig |
| Production Build | Optimiert, App Store ready, EAS Submit | Kein Hot-Reload, Sign-Certificates erforderlich |
# Development Build erstellen
npx expo install expo-dev-client
eas build --profile development --platform ios
# SDK-Version prüfen und upgraden
npx expo install --check
npx expo install --fix
2. Expo Router — File-based Navigation
Expo Router bringt das bewährte Next.js-Prinzip der dateibasierten Navigation zu React Native. Claude Code strukturiert die app/-Ordner so, dass Routen automatisch aus der Verzeichnisstruktur entstehen — kein manuelles Stack-Konfigurieren mehr.
Ordnerstruktur
app/
├── _layout.tsx # Root Layout (Fonts, Provider)
├── index.tsx # Home-Screen (/)
├── (tabs)/
│ ├── _layout.tsx # Tab-Navigator
│ ├── index.tsx # Tab: Home
│ ├── explore.tsx # Tab: Explore
│ └── profile.tsx # Tab: Profile
├── (auth)/
│ ├── _layout.tsx # Auth-Stack
│ ├── login.tsx
│ └── register.tsx
├── product/
│ └── [id].tsx # Dynamische Route: /product/123
└── +not-found.tsx # 404-Screen
Root Layout — _layout.tsx
import { Stack } from 'expo-router';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
});
useEffect(() => {
if (loaded) SplashScreen.hideAsync();
}, [loaded]);
if (!loaded) return null;
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
);
}
Tab Navigator
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs screenOptions={{
tabBarActiveTintColor: '#818cf8',
tabBarStyle: { backgroundColor: '#0a0a0f', borderTopColor: '#1e1e3a' },
}}>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) =>
<Ionicons name="home" size={24} color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profil',
tabBarIcon: ({ color }) =>
<Ionicons name="person" size={24} color={color} />,
}}
/>
</Tabs>
);
}
Navigation mit Link, useRouter und useLocalSearchParams
import { Link, useRouter, useLocalSearchParams } from 'expo-router';
import { View, Text, Button } from 'react-native';
// Deklarative Navigation via Link-Komponente
function HomeScreen() {
const router = useRouter();
return (
<View>
{/* Deklarativ */}
<Link href="/product/42">Produkt öffnen</Link>
{/* Imperativ */}
<Button
title="Zum Profil"
onPress={() => router.push('/profile')}
/>
{/* Mit Parametern */}
<Button
title="Artikel öffnen"
onPress={() => router.push({ pathname: '/product/[id]', params: { id: '99' } })}
/>
</View>
);
}
// Dynamische Route: app/product/[id].tsx
function ProductScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>Produkt ID: {id}</Text>;
}
3. NativeWind — Tailwind CSS für React Native
NativeWind bringt Tailwind CSS-Klassen zu React Native. Claude Code setzt className-Props auf native Komponenten ein — dieselben Utility-Klassen wie im Web, aber optimiert für iOS und Android.
Installation und Setup
# Installation
npm install nativewind tailwindcss
npx tailwindcss init
# tailwind.config.js
module.exports = {
content: ['./app/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'],
presets: [require('nativewind/preset')],
theme: {
extend: {
colors: {
primary: '#818cf8',
surface: '#0a0a0f',
}
}
},
plugins: [],
};
// babel.config.js
module.exports = {
presets: ['babel-preset-expo'],
plugins: ['nativewind/babel'],
};
Komponenten mit className
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import { useColorScheme } from 'nativewind';
export function ProductCard({ title, price }: { title: string; price: number }) {
const { colorScheme } = useColorScheme();
return (
<View className="bg-white dark:bg-gray-900 rounded-2xl p-4 mx-4 mb-3 shadow-md">
<Text className="text-xl font-bold text-gray-900 dark:text-white mb-1">
{title}
</Text>
<Text className="text-indigo-500 font-semibold text-lg">
€{price.toFixed(2)}
</Text>
<TouchableOpacity className="bg-indigo-500 active:bg-indigo-700 rounded-xl py-3 mt-3 items-center">
<Text className="text-white font-semibold">In den Warenkorb</Text>
</TouchableOpacity>
</View>
);
}
// Dark Mode automatisch mit useColorScheme
export function ThemedScreen() {
const { colorScheme, toggleColorScheme } = useColorScheme();
const isDark = colorScheme === 'dark';
return (
<ScrollView className="flex-1 bg-white dark:bg-gray-950">
<Text className="text-gray-900 dark:text-white text-2xl font-bold p-6">
{isDark ? 'Dark Mode aktiv' : 'Light Mode aktiv'}
</Text>
</ScrollView>
);
}
Platform-specific Styles
import { Platform, View, Text } from 'react-native';
// Methode 1: Platform.select
const styles = {
container: Platform.select({
ios: 'pt-14 px-4', // Safe Area iOS
android: 'pt-6 px-4', // Android StatusBar
default: 'pt-4 px-4',
}),
};
// Methode 2: Platform-Dateien
// Header.ios.tsx → wird automatisch auf iOS geladen
// Header.android.tsx → wird automatisch auf Android geladen
// Methode 3: Inline className mit Platform-Check
function SafeContainer({ children }: { children: React.ReactNode }) {
return (
<View className={`flex-1 ${Platform.OS === 'ios' ? 'pt-14' : 'pt-6'}`}>
{children}
</View>
);
}
Tipp von Claude Code: NativeWind v4 unterstützt Tailwind v3 vollständig. Für Custom-Fonts und Custom-Colors empfiehlt Claude Code die Erweiterung des Tailwind-Themes in tailwind.config.js statt inline-Styles.
4. Native Modules — Camera, Location, Notifications und mehr
Expo stellt eine umfangreiche Bibliothek vorgefertigter nativer Module bereit. Claude Code integriert sie via Config Plugins automatisch in den Build — ohne manuelle Änderungen an Xcode oder Android Studio.
expo-camera — Kamerazugriff
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useState, useRef } from 'react';
import { View, Text, Button } from 'react-native';
export function CameraScreen() {
const [permission, requestPermission] = useCameraPermissions();
const [facing, setFacing] = useState<'front' | 'back'>('back');
const cameraRef = useRef<CameraView>(null);
if (!permission) return <View />;
if (!permission.granted) {
return (
<View className="flex-1 items-center justify-center">
<Text>Kamera-Zugriff erforderlich</Text>
<Button title="Erlauben" onPress={requestPermission} />
</View>
);
}
async function takePicture() {
const photo = await cameraRef.current?.takePictureAsync({ quality: 0.8 });
console.log('Foto URI:', photo?.uri);
}
return (
<CameraView ref={cameraRef} style={{ flex: 1 }} facing={facing}>
<Button title="📷 Foto" onPress={takePicture} />
<Button title="🔄 Wechseln" onPress={() => setFacing(f => f === 'back' ? 'front' : 'back')} />
</CameraView>
);
}
expo-location — GPS und Standort
import * as Location from 'expo-location';
import { useEffect, useState } from 'react';
export function useCurrentLocation() {
const [location, setLocation] = useState<Location.LocationObject | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Standort-Zugriff verweigert');
return;
}
const loc = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High });
setLocation(loc);
})();
}, []);
return { location, error };
}
expo-secure-store und expo-file-system
import * as SecureStore from 'expo-secure-store';
import * as FileSystem from 'expo-file-system';
// Sicher speichern (Keychain/Keystore)
await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_token');
// Datei-Download
const fileUri = FileSystem.documentDirectory + 'report.pdf';
const { uri } = await FileSystem.downloadAsync(
'https://api.example.com/report.pdf',
fileUri
);
console.log('Gespeichert unter:', uri);
// Datei-Infos
const info = await FileSystem.getInfoAsync(fileUri);
if (info.exists) console.log('Größe:', info.size, 'bytes');
Wichtig: Native Module wie expo-camera und expo-location funktionieren NICHT in Expo Go, wenn sie als Config Plugin in app.json konfiguriert sind. Claude Code wechselt automatisch auf Development Builds für solche Features.
5. EAS Build und Submit — App Store und Play Store
EAS (Expo Application Services) ist Explos Cloud-Build-Plattform. Claude Code richtet die komplette Build-Pipeline ein — von der eas.json-Konfiguration bis zum automatischen App Store Submit.
EAS Setup
# EAS CLI installieren
npm install -g eas-cli
# Bei Expo einloggen
eas login
# Projekt initialisieren
eas init
eas.json — Build-Profile
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true }
},
"preview": {
"distribution": "internal",
"ios": { "enterpriseProvisioning": "adhoc" }
},
"production": {
"autoIncrement": true,
"ios": { "resourceClass": "m-medium" },
"android": { "buildType": "aab" }
}
},
"submit": {
"production": {
"ios": {
"appleId": "dev@example.com",
"ascAppId": "1234567890",
"appleTeamId": "ABC1234DEF"
},
"android": {
"serviceAccountKeyPath": "./google-service-account.json",
"track": "internal"
}
}
}
}
Build-Befehle
# Development Build (Simulator)
eas build --profile development --platform ios
# Production Build für beide Plattformen
eas build --profile production --platform all
# Nur Android AAB (für Google Play)
eas build --profile production --platform android
# Nur iOS IPA (für App Store)
eas build --profile production --platform ios
# Build-Status prüfen
eas build:list
eas submit — App Store und Play Store
# iOS → App Store Connect (TestFlight)
eas submit --platform ios --latest
# Android → Google Play (Internal Track)
eas submit --platform android --latest
# Beide gleichzeitig
eas submit --platform all --latest
# Spezifischen Build submittieren
eas submit --platform ios --id BUILD_ID
⚙️ CI/CD mit EAS + GitHub Actions
Claude Code richtet automatisch eine GitHub Actions Pipeline ein, die bei jedem Push auf main einen EAS Production Build triggert und nach erfolgreichem Build direkt submitted.
- eas build --non-interactive
- eas submit --non-interactive
- EXPO_TOKEN als Secret
- Auto-Increment Version
6. OTA Updates und Push Notifications
Mit expo-updates lassen sich JavaScript-Änderungen ohne App Store Review direkt auf Nutzergeräte pushen. Kombiniert mit dem Expo Notification Service entsteht eine vollständige Post-Launch-Infrastruktur.
expo-updates — OTA Updates
# Installation
npx expo install expo-updates
import * as Updates from 'expo-updates';
import { useEffect, useState } from 'react';
import { Alert } from 'react-native';
export function useAppUpdates() {
const [updateAvailable, setUpdateAvailable] = useState(false);
useEffect(() => {
async function checkForUpdates() {
if (__DEV__) return; // Nur in Production
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
setUpdateAvailable(true);
await Updates.fetchUpdateAsync();
Alert.alert(
'Update verfügbar',
'Ein neues Update wurde geladen. App neu starten?',
[{ text: 'Neustart', onPress: () => Updates.reloadAsync() }]
);
}
} catch (e) {
console.warn('Update-Prüfung fehlgeschlagen:', e);
}
}
checkForUpdates();
}, []);
return { updateAvailable };
}
eas update — Update deployen
# Update auf production-Channel deployen
eas update --channel production --message "Bugfix: Checkout-Fehler behoben"
# Update auf staging deployen
eas update --channel staging --message "Neues Feature: Dark Mode"
# Rollback — vorheriges Update reaktivieren
eas channel:edit production --rollout-percentage 50 # Canary Release
# Update-Verlauf anzeigen
eas update:list --branch production
Push Notifications mit expo-notifications
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
// Notification-Handler konfigurieren
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function registerForPushNotifications(): Promise<string | undefined> {
if (!Device.isDevice) {
console.warn('Push Notifications nur auf echten Geräten');
return;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') return;
// Expo Push Token abrufen
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
// Android Notification-Kanal
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Standard',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#818cf8',
});
}
return token;
}
// Push Notification senden (Server-Side via Expo Push API)
async function sendPushNotification(expoPushToken: string, title: string, body: string) {
const message = {
to: expoPushToken,
sound: 'default',
title,
body,
data: { screen: 'home' },
badge: 1,
};
await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
}
Lokale Notifications (ohne Server)
// Geplante Notification (z. B. Reminder)
await Notifications.scheduleNotificationAsync({
content: {
title: '🏋️ Workout-Zeit!',
body: 'Dein tägliches Training wartet auf dich.',
data: { type: 'workout-reminder' },
},
trigger: {
hour: 8,
minute: 30,
repeats: true,
},
});
// Sofortige lokale Notification
await Notifications.scheduleNotificationAsync({
content: { title: 'Erledigt!', body: 'Dein Export ist fertig.' },
trigger: null, // Sofort
});
// Alle Notifications entfernen
await Notifications.cancelAllScheduledNotificationsAsync();
🚀 Expo Notification Service vs. FCM/APNs direkt
Der Expo Push Notification Service leitet an FCM (Android) und APNs (iOS) weiter — ohne dass du eigene Zertifikate oder Firebase-Integration einrichten musst. Für Production-Apps mit hohem Volumen empfiehlt Claude Code den direkten FCM/APNs-Zugang via EAS.
Claude Code konfiguriert beides — einfacher Start mit Expo ENS, Migration zu direktem FCM/APNs bei Bedarf.
Notification-Listener in der App
import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';
import { useRouter } from 'expo-router';
export function useNotificationNavigation() {
const router = useRouter();
const notificationListener = useRef<Notifications.EventSubscription>();
const responseListener = useRef<Notifications.EventSubscription>();
useEffect(() => {
// Notification empfangen (App im Vordergrund)
notificationListener.current = Notifications.addNotificationReceivedListener(n => {
console.log('Notification erhalten:', n.request.content.title);
});
// Nutzer tippt auf Notification
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
const data = response.notification.request.content.data;
if (data.screen) router.push(`/${data.screen}`);
});
return () => {
notificationListener.current?.remove();
responseListener.current?.remove();
};
}, []);
}
Mit Claude Code Mobile Apps bauen
Claude Code richtet Expo-Projekte vollständig ein — von der ersten Zeile Code bis zum App Store Submit. Starte jetzt deinen kostenlosen Trial.
Kostenlosen Trial starten →