Skip to content
IRC-Coding IRC-Coding
Mobile App Entwicklung React Native Flutter Swift Kotlin PWA Cross Platform Native Hybrid

Mobile App-Entwicklung: React Native, Flutter, Swift, Kotlin & PWA Cross-Platform

Mobile App-Entwicklung mit React Native, Flutter, Swift, Kotlin und PWA. Cross-Platform-Strategien, Native vs. Hybrid, Performance-Vergleich und Best Practices.

S

schutzgeist

2 min read

Mobile App-Entwicklung: React Native, Flutter, Swift, Kotlin & PWA Cross-Platform

Dieser Beitrag ist eine umfassende Einführung in die Mobile App-Entwicklung – inklusive React Native, Flutter, Swift, Kotlin und PFA mit Cross-Platform-Strategien und praktischen Beispielen.

In a Nutshell

Mobile App-Entwicklung umfasst Native (iOS/Android), Cross-Platform (React Native, Flutter) und Web-Apps (PWA). Native bietet beste Performance, Cross-Platform beschleunigt Entwicklung, PWA vereinfacht Verteilung.

Kompakte Fachbeschreibung

Mobile App-Entwicklung ist die Erstellung von Anwendungen für mobile Geräte wie Smartphones und Tablets mit verschiedenen technologischen Ansätzen.

Entwicklungsansätze:

Native Entwicklung

  • iOS: Swift mit UIKit oder SwiftUI
  • Android: Kotlin mit Jetpack Compose
  • Vorteile: Beste Performance, volle API-Nutzung
  • Nachteile: Plattform-spezifische Entwicklung

Cross-Platform Entwicklung

  • React Native: JavaScript/React für iOS & Android
  • Flutter: Dart von Google für iOS & Android
  • Vorteile: Code-Wiederverwendung, schnellere Entwicklung
  • Nachteile: Performance-Overhead, eingeschränkter API-Zugriff

Progressive Web Apps (PWA)

  • Technologie: HTML5, CSS3, JavaScript mit Service Workers
  • Vorteile: Ein Codebase, App-Store-fähig, offline-fähig
  • Nachteile: Eingeschränkte Hardware-Zugriffe, Browser-Abhängigkeit

Prüfungsrelevante Stichpunkte

  • Mobile App-Entwicklung: Erstellung von Anwendungen für mobile Geräte
  • Native: Plattform-spezifische Entwicklung (Swift/Kotlin)
  • Cross-Platform: Ein Codebase für mehrere Plattformen
  • React Native: JavaScript-basierte Cross-Platform-Lösung
  • Flutter: Dart-basierte Cross-Platform-Lösung von Google
  • PWA: Progressive Web Apps mit App-ähnlichem Verhalten
  • UI/UX: User Interface und User Experience Design
  • IHK-relevant: Moderne mobile Anwendungsentwicklung

Kernkomponenten

  1. Plattform-Auswahl: Native vs. Cross-Platform vs. PWA
  2. Entwicklungs-Tools: IDEs, SDKs, Frameworks
  3. UI-Komponenten: Native Controls vs. Custom Components
  4. State Management: Datenfluss und Zustandsverwaltung
  5. Navigation: Screen-Navigation und Routing
  6. Performance: Optimierung und Memory Management
  7. Testing: Unit, Integration und UI-Tests
  8. Deployment: App Store und Distribution

Praxisbeispiele

1. React Native App mit TypeScript

// App.tsx - Hauptanwendung
import React from 'react';
import {
  SafeAreaView,
  StyleSheet,
  ScrollView,
  View,
  Text,
  StatusBar,
  TouchableOpacity,
  Alert,
  ActivityIndicator
} from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { Provider, useSelector, useDispatch } from 'react-redux';
import { store } from './store';
import { fetchUsers, createUser, updateUser, deleteUser } from './store/userSlice';

// Typen definieren
interface User {
  id: string;
  name: string;
  email: string;
  phone: string;
  address: string;
  createdAt: string;
  updatedAt: string;
}

interface RootState {
  users: {
    data: User[];
    loading: boolean;
    error: string | null;
  };
}

// User List Component
const UserListScreen: React.FC = ({ navigation }) => {
  const dispatch = useDispatch();
  const { data: users, loading, error } = useSelector((state: RootState) => state.users);

  React.useEffect(() => {
    dispatch(fetchUsers());
  }, [dispatch]);

  const handleAddUser = () => {
    navigation.navigate('UserForm', { mode: 'create' });
  };

  const handleEditUser = (user: User) => {
    navigation.navigate('UserForm', { mode: 'edit', user });
  };

  const handleDeleteUser = (user: User) => {
    Alert.alert(
      'Benutzer löschen',
      `Möchten Sie ${user.name} wirklich löschen?`,
      [
        { text: 'Abbrechen', style: 'cancel' },
        {
          text: 'Löschen',
          style: 'destructive',
          onPress: () => dispatch(deleteUser(user.id)),
        },
      ]
    );
  };

  if (loading) {
    return (
      <View style={styles.centerContainer}>
        <ActivityIndicator size="large" color="#2E86AB" />
        <Text style={styles.loadingText}>Lade Benutzer...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.centerContainer}>
        <Text style={styles.errorText}>Fehler: {error}</Text>
        <TouchableOpacity
          style={styles.retryButton}
          onPress={() => dispatch(fetchUsers())}
        >
          <Text style={styles.retryButtonText}>Erneut versuchen</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
      
      <View style={styles.header}>
        <Text style={styles.headerTitle}>Benutzerliste</Text>
        <TouchableOpacity style={styles.addButton} onPress={handleAddUser}>
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>

      <ScrollView style={styles.scrollView}>
        {users.map((user) => (
          <View key={user.id} style={styles.userCard}>
            <View style={styles.userInfo}>
              <Text style={styles.userName}>{user.name}</Text>
              <Text style={styles.userEmail}>{user.email}</Text>
              <Text style={styles.userPhone}>{user.phone}</Text>
              <Text style={styles.userAddress}>{user.address}</Text>
            </View>
            <View style={styles.userActions}>
              <TouchableOpacity
                style={[styles.actionButton, styles.editButton]}
                onPress={() => handleEditUser(user)}
              >
                <Text style={styles.actionButtonText}>Bearbeiten</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={[styles.actionButton, styles.deleteButton]}
                onPress={() => handleDeleteUser(user)}
              >
                <Text style={styles.actionButtonText}>Löschen</Text>
              </TouchableOpacity>
            </View>
          </View>
        ))}
      </ScrollView>
    </SafeAreaView>
  );
};

// User Form Component
const UserFormScreen: React.FC = ({ route, navigation }) => {
  const dispatch = useDispatch();
  const { mode, user } = route.params as { mode: 'create' | 'edit'; user?: User };
  const [formData, setFormData] = React.useState<Partial<User>>(
    user || {
      name: '',
      email: '',
      phone: '',
      address: '',
    }
  );
  const [errors, setErrors] = React.useState<Record<string, string>>({});
  const [loading, setLoading] = React.useState(false);

  const validateForm = (): boolean => {
    const newErrors: Record<string, string> = {};

    if (!formData.name?.trim()) {
      newErrors.name = 'Name ist erforderlich';
    }

    if (!formData.email?.trim()) {
      newErrors.email = 'E-Mail ist erforderlich';
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
      newErrors.email = 'Ungültige E-Mail-Adresse';
    }

    if (!formData.phone?.trim()) {
      newErrors.phone = 'Telefon ist erforderlich';
    }

    if (!formData.address?.trim()) {
      newErrors.address = 'Adresse ist erforderlich';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async () => {
    if (!validateForm()) {
      return;
    }

    setLoading(true);
    try {
      if (mode === 'create') {
        await dispatch(createUser(formData as Omit<User, 'id' | 'createdAt' | 'updatedAt'>));
        Alert.alert('Erfolg', 'Benutzer wurde erstellt');
      } else {
        await dispatch(updateUser({ ...formData, id: user!.id } as User));
        Alert.alert('Erfolg', 'Benutzer wurde aktualisiert');
      }
      navigation.goBack();
    } catch (error) {
      Alert.alert('Fehler', 'Ein Fehler ist aufgetreten');
    } finally {
      setLoading(false);
    }
  };

  const updateField = (field: keyof User, value: string) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: '' }));
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
      
      <View style={styles.formHeader}>
        <TouchableOpacity onPress={() => navigation.goBack()}>
          <Text style={styles.cancelButton}>Abbrechen</Text>
        </TouchableOpacity>
        <Text style={styles.formTitle}>
          {mode === 'create' ? 'Benutzer erstellen' : 'Benutzer bearbeiten'}
        </Text>
        <TouchableOpacity onPress={handleSubmit} disabled={loading}>
          <Text style={[styles.saveButton, loading && styles.disabledButton]}>
            {loading ? 'Speichern...' : 'Speichern'}
          </Text>
        </TouchableOpacity>
      </View>

      <ScrollView style={styles.formScrollView}>
        <View style={styles.form}>
          <View style={styles.formGroup}>
            <Text style={styles.label}>Name *</Text>
            <TextInput
              style={[styles.input, errors.name && styles.inputError]}
              value={formData.name}
              onChangeText={(value) => updateField('name', value)}
              placeholder="Name eingeben"
              editable={!loading}
            />
            {errors.name && <Text style={styles.errorText}>{errors.name}</Text>}
          </View>

          <View style={styles.formGroup}>
            <Text style={styles.label}>E-Mail *</Text>
            <TextInput
              style={[styles.input, errors.email && styles.inputError]}
              value={formData.email}
              onChangeText={(value) => updateField('email', value)}
              placeholder="E-Mail eingeben"
              keyboardType="email-address"
              autoCapitalize="none"
              editable={!loading}
            />
            {errors.email && <Text style={styles.errorText}>{errors.email}</Text>}
          </View>

          <View style={styles.formGroup}>
            <Text style={styles.label}>Telefon *</Text>
            <TextInput
              style={[styles.input, errors.phone && styles.inputError]}
              value={formData.phone}
              onChangeText={(value) => updateField('phone', value)}
              placeholder="Telefon eingeben"
              keyboardType="phone-pad"
              editable={!loading}
            />
            {errors.phone && <Text style={styles.errorText}>{errors.phone}</Text>}
          </View>

          <View style={styles.formGroup}>
            <Text style={styles.label}>Adresse *</Text>
            <TextInput
              style={[styles.input, errors.address && styles.inputError]}
              value={formData.address}
              onChangeText={(value) => updateField('address', value)}
              placeholder="Adresse eingeben"
              multiline
              numberOfLines={3}
              editable={!loading}
            />
            {errors.address && <Text style={styles.errorText}>{errors.address}</Text>}
          </View>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

// Navigation Stack
const Stack = createStackNavigator();

const App: React.FC = () => {
  return (
    <Provider store={store}>
      <NavigationContainer>
        <Stack.Navigator initialRouteName="UserList">
          <Stack.Screen
            name="UserList"
            component={UserListScreen}
            options={{
              title: 'Benutzer-App',
              headerStyle: {
                backgroundColor: '#2E86AB',
              },
              headerTintColor: '#ffffff',
            }}
          />
          <Stack.Screen
            name="UserForm"
            component={UserFormScreen}
            options={{
              title: 'Benutzer-Formular',
              headerStyle: {
                backgroundColor: '#2E86AB',
              },
              headerTintColor: '#ffffff',
            }}
          />
        </Stack.Navigator>
      </NavigationContainer>
    </Provider>
  );
};

// Styles
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#ffffff',
  },
  centerContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  headerTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#333333',
  },
  addButton: {
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: '#2E86AB',
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonText: {
    color: '#ffffff',
    fontSize: 24,
    fontWeight: 'bold',
  },
  scrollView: {
    flex: 1,
    padding: 16,
  },
  userCard: {
    backgroundColor: '#ffffff',
    borderRadius: 8,
    padding: 16,
    marginBottom: 12,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  userInfo: {
    marginBottom: 12,
  },
  userName: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#333333',
    marginBottom: 4,
  },
  userEmail: {
    fontSize: 14,
    color: '#666666',
    marginBottom: 2,
  },
  userPhone: {
    fontSize: 14,
    color: '#666666',
    marginBottom: 2,
  },
  userAddress: {
    fontSize: 14,
    color: '#666666',
  },
  userActions: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  actionButton: {
    flex: 1,
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 4,
    marginHorizontal: 4,
  },
  editButton: {
    backgroundColor: '#2E86AB',
  },
  deleteButton: {
    backgroundColor: '#C73E1D',
  },
  actionButtonText: {
    color: '#ffffff',
    textAlign: 'center',
    fontSize: 14,
    fontWeight: 'bold',
  },
  loadingText: {
    marginTop: 16,
    fontSize: 16,
    color: '#666666',
  },
  errorText: {
    marginTop: 16,
    fontSize: 16,
    color: '#C73E1D',
    textAlign: 'center',
  },
  retryButton: {
    marginTop: 16,
    paddingVertical: 12,
    paddingHorizontal: 24,
    backgroundColor: '#2E86AB',
    borderRadius: 8,
  },
  retryButtonText: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: 'bold',
  },
  formHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  cancelButton: {
    fontSize: 16,
    color: '#666666',
  },
  formTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#333333',
  },
  saveButton: {
    fontSize: 16,
    color: '#2E86AB',
    fontWeight: 'bold',
  },
  disabledButton: {
    color: '#cccccc',
  },
  formScrollView: {
    flex: 1,
  },
  form: {
    padding: 16,
  },
  formGroup: {
    marginBottom: 20,
  },
  label: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333333',
    marginBottom: 8,
  },
  input: {
    borderWidth: 1,
    borderColor: '#e0e0e0',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    color: '#333333',
  },
  inputError: {
    borderColor: '#C73E1D',
  },
  errorText: {
    marginTop: 4,
    fontSize: 14,
    color: '#C73E1D',
  },
});

export default App;

2. Flutter App mit Dart

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';

// Redux State
class AppState {
  final List<User> users;
  final bool isLoading;
  final String? error;

  AppState({
    required this.users,
    required this.isLoading,
    this.error,
  });

  AppState copyWith({
    List<User>? users,
    bool? isLoading,
    String? error,
  }) {
    return AppState(
      users: users ?? this.users,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
    );
  }
}

// User Model
class User {
  final String id;
  final String name;
  final String email;
  final String phone;
  final String address;
  final DateTime createdAt;
  final DateTime updatedAt;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.phone,
    required this.address,
    required this.createdAt,
    required this.updatedAt,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
      phone: json['phone'],
      address: json['address'],
      createdAt: DateTime.parse(json['createdAt']),
      updatedAt: DateTime.parse(json['updatedAt']),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'phone': phone,
      'address': address,
      'createdAt': createdAt.toIso8601String(),
      'updatedAt': updatedAt.toIso8601String(),
    };
  }

  User copyWith({
    String? name,
    String? email,
    String? phone,
    String? address,
  }) {
    return User(
      id: id,
      name: name ?? this.name,
      email: email ?? this.email,
      phone: phone ?? this.phone,
      address: address ?? this.address,
      createdAt: createdAt,
      updatedAt: DateTime.now(),
    );
  }
}

// Redux Actions
enum UserAction { fetch, create, update, delete }

class FetchUsersAction {}

class CreateUsersAction {
  final User user;
  CreateUsersAction(this.user);
}

class UpdateUsersAction {
  final User user;
  UpdateUsersAction(this.user);
}

class DeleteUsersAction {
  final String userId;
  DeleteUsersAction(this.userId);
}

class SetLoadingAction {
  final bool isLoading;
  SetLoadingAction(this.isLoading);
}

class SetErrorAction {
  final String error;
  SetErrorAction(this.error);
}

// API Service
class ApiService {
  static const String baseUrl = 'https://api.example.com/users';

  static Future<List<User>> getUsers() async {
    try {
      final response = await http.get(Uri.parse(baseUrl));
      if (response.statusCode == 200) {
        List<dynamic> data = json.decode(response.body);
        return data.map((json) => User.fromJson(json)).toList();
      } else {
        throw Exception('Failed to load users');
      }
    } catch (e) {
      throw Exception('Network error: $e');
    }
  }

  static Future<User> createUser(User user) async {
    try {
      final response = await http.post(
        Uri.parse(baseUrl),
        headers: {'Content-Type': 'application/json'},
        body: json.encode(user.toJson()),
      );
      if (response.statusCode == 201) {
        return User.fromJson(json.decode(response.body));
      } else {
        throw Exception('Failed to create user');
      }
    } catch (e) {
      throw Exception('Network error: $e');
    }
  }

  static Future<User> updateUser(User user) async {
    try {
      final response = await http.put(
        Uri.parse('$baseUrl/${user.id}'),
        headers: {'Content-Type': 'application/json'},
        body: json.encode(user.toJson()),
      );
      if (response.statusCode == 200) {
        return User.fromJson(json.decode(response.body));
      } else {
        throw Exception('Failed to update user');
      }
    } catch (e) {
      throw Exception('Network error: $e');
    }
  }

  static Future<void> deleteUser(String userId) async {
    try {
      final response = await http.delete(Uri.parse('$baseUrl/$userId'));
      if (response.statusCode != 204) {
        throw Exception('Failed to delete user');
      }
    } catch (e) {
      throw Exception('Network error: $e');
    }
  }
}

// Redux Reducer
AppState appReducer(AppState state, dynamic action) {
  switch (action.runtimeType) {
    case SetLoadingAction:
      return state.copyWith(isLoading: (action as SetLoadingAction).isLoading);
    
    case SetErrorAction:
      return state.copyWith(error: (action as SetErrorAction).error, isLoading: false);
    
    case FetchUsersAction:
      return state.copyWith(isLoading: true, error: null);
    
    case CreateUsersAction:
      return state.copyWith(isLoading: true, error: null);
    
    case UpdateUsersAction:
      return state.copyWith(isLoading: true, error: null);
    
    case DeleteUsersAction:
      return state.copyWith(isLoading: true, error: null);
    
    default:
      return state;
  }
}

// Middleware for async actions
class UsersMiddleware extends MiddlewareClass<AppState> {
  @override
  AppState call(Store<AppState> store, dynamic action, NextDispatcher next) {
    if (action is FetchUsersAction) {
      _fetchUsers(store);
    } else if (action is CreateUsersAction) {
      _createUser(store, action.user);
    } else if (action is UpdateUsersAction) {
      _updateUser(store, action.user);
    } else if (action is DeleteUsersAction) {
      _deleteUser(store, action.userId);
    }
    
    return next(action);
  }

  Future<void> _fetchUsers(Store<AppState> store) async {
    try {
      store.dispatch(SetLoadingAction(true));
      final users = await ApiService.getUsers();
      store.dispatch(SetUsersAction(users));
      store.dispatch(SetLoadingAction(false));
    } catch (e) {
      store.dispatch(SetErrorAction('Failed to fetch users: $e'));
    }
  }

  Future<void> _createUser(Store<AppState> store, User user) async {
    try {
      store.dispatch(SetLoadingAction(true));
      final createdUser = await ApiService.createUser(user);
      final users = List<User>.from(store.state.users)..add(createdUser);
      store.dispatch(SetUsersAction(users));
      store.dispatch(SetLoadingAction(false));
    } catch (e) {
      store.dispatch(SetErrorAction('Failed to create user: $e'));
    }
  }

  Future<void> _updateUser(Store<AppState> store, User user) async {
    try {
      store.dispatch(SetLoadingAction(true));
      final updatedUser = await ApiService.updateUser(user);
      final users = store.state.users.map((u) => u.id == user.id ? updatedUser : u).toList();
      store.dispatch(SetUsersAction(users));
      store.dispatch(SetLoadingAction(false));
    } catch (e) {
      store.dispatch(SetErrorAction('Failed to update user: $e'));
    }
  }

  Future<void> _deleteUser(Store<AppState> store, String userId) async {
    try {
      store.dispatch(SetLoadingAction(true));
      await ApiService.deleteUser(userId);
      final users = store.state.users.where((u) => u.id != userId).toList();
      store.dispatch(SetUsersAction(users));
      store.dispatch(SetLoadingAction(false));
    } catch (e) {
      store.dispatch(SetErrorAction('Failed to delete user: $e'));
    }
  }
}

class SetUsersAction {
  final List<User> users;
  SetUsersAction(this.users);
}

// Main App
void main() {
  final store = Store<AppState>(
    appReducer,
    initialState: AppState(users: [], isLoading: false),
    middleware: [UsersMiddleware()],
  );

  runApp(MyApp(store: store));
}

class MyApp extends StatelessWidget {
  final Store<AppState> store;

  const MyApp({Key? key, required this.store}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        title: 'Flutter User App',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: UserListScreen(),
      ),
    );
  }
}

// User List Screen
class UserListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Benutzerliste'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        elevation: 2,
      ),
      body: StoreConnector<AppState, _ViewModel>(
        converter: (store) => _ViewModel(
          users: store.state.users,
          isLoading: store.state.isLoading,
          error: store.state.error,
        ),
        builder: (context, viewModel) {
          if (viewModel.isLoading) {
            return const Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  CircularProgressIndicator(),
                  SizedBox(height: 16),
                  Text('Lade Benutzer...'),
                ],
              ),
            );
          }

          if (viewModel.error != null) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.error, size: 64, color: Colors.red),
                  SizedBox(height: 16),
                  Text(
                    'Fehler: ${viewModel.error}',
                    style: TextStyle(color: Colors.red),
                    textAlign: TextAlign.center,
                  ),
                  SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () => StoreProvider.of<AppState>(context).dispatch(FetchUsersAction()),
                    child: Text('Erneut versuchen'),
                  ),
                ],
              ),
            );
          }

          if (viewModel.users.isEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.people_outline, size: 64, color: Colors.grey),
                  SizedBox(height: 16),
                  Text(
                    'Keine Benutzer gefunden',
                    style: TextStyle(color: Colors.grey),
                  ),
                ],
              ),
            );
          }

          return RefreshIndicator(
            onRefresh: () async {
              StoreProvider.of<AppState>(context).dispatch(FetchUsersAction());
            },
            child: ListView.builder(
              padding: const EdgeInsets.all(16),
              itemCount: viewModel.users.length,
              itemBuilder: (context, index) {
                final user = viewModel.users[index];
                return UserCard(user: user);
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => UserFormScreen()),
          );
        },
        backgroundColor: Colors.blue,
        child: const Icon(Icons.add),
      ),
    );
  }
}

// ViewModel
class _ViewModel {
  final List<User> users;
  final bool isLoading;
  final String? error;

  _ViewModel({
    required this.users,
    required this.isLoading,
    this.error,
  });
}

// User Card Widget
class UserCard extends StatelessWidget {
  final User user;

  const UserCard({Key? key, required this.user}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      margin: const EdgeInsets.only(bottom: 16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                CircleAvatar(
                  backgroundColor: Colors.blue,
                  child: Text(
                    user.name.isNotEmpty ? user.name[0].toUpperCase() : 'U',
                    style: TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        user.name,
                        style: TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      Text(
                        user.email,
                        style: TextStyle(
                          fontSize: 14,
                          color: Colors.grey[600],
                        ),
                      ),
                    ],
                  ),
                ),
                PopupMenuButton(
                  itemBuilder: (context) => [
                    PopupMenuItem(
                      value: 'edit',
                      child: Row(
                        children: [
                          Icon(Icons.edit),
                          SizedBox(width: 8),
                          Text('Bearbeiten'),
                        ],
                      ),
                    ),
                    PopupMenuItem(
                      value: 'delete',
                      child: Row(
                        children: [
                          Icon(Icons.delete, color: Colors.red),
                          SizedBox(width: 8),
                          Text('Löschen', style: TextStyle(color: Colors.red)),
                        ],
                      ),
                    ),
                  ],
                  onSelected: (value) {
                    if (value == 'edit') {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => UserFormScreen(user: user),
                        ),
                      );
                    } else if (value == 'delete') {
                      _showDeleteDialog(context);
                    }
                  },
                ),
              ],
            ),
            const SizedBox(height: 12),
            _buildInfoRow(Icons.phone, user.phone),
            _buildInfoRow(Icons.location_on, user.address),
            const SizedBox(height: 8),
            Text(
              'Erstellt: ${_formatDate(user.createdAt)}',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[500],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoRow(IconData icon, String text) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 4),
      child: Row(
        children: [
          Icon(icon, size: 16, color: Colors.grey[600]),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              text,
              style: TextStyle(
                fontSize: 14,
                color: Colors.grey[600],
              ),
            ),
          ),
        ],
      ),
    );
  }

  String _formatDate(DateTime date) {
    return '${date.day}.${date.month}.${date.year}';
  }

  void _showDeleteDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Benutzer löschen'),
        content: Text('Möchten Sie ${user.name} wirklich löschen?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('Abbrechen'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              StoreProvider.of<AppState>(context).dispatch(DeleteUsersAction(user.id));
            },
            child: Text(
              'Löschen',
              style: TextStyle(color: Colors.red),
            ),
          ),
        ],
      ),
    );
  }
}

// User Form Screen
class UserFormScreen extends StatefulWidget {
  final User? user;

  const UserFormScreen({Key? key, this.user}) : super(key: key);

  @override
  _UserFormScreenState createState() => _UserFormScreenState();
}

class _UserFormScreenState extends State<UserFormScreen> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _phoneController = TextEditingController();
  final _addressController = TextEditingController();

  bool _isLoading = false;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    if (widget.user != null) {
      _nameController.text = widget.user!.name;
      _emailController.text = widget.user!.email;
      _phoneController.text = widget.user!.phone;
      _addressController.text = widget.user!.address;
    }
  }

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _phoneController.dispose();
    _addressController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.user == null ? 'Benutzer erstellen' : 'Benutzer bearbeiten'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        actions: [
          if (!_isLoading)
            TextButton(
              onPressed: _saveUser,
              child: Text(
                'Speichern',
                style: TextStyle(color: Colors.white),
              ),
            )
          else
            Padding(
              padding: const EdgeInsets.all(16),
              child: SizedBox(
                width: 20,
                height: 20,
                child: CircularProgressIndicator(
                  strokeWidth: 2,
                  valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                ),
              ),
            ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              if (_errorMessage != null)
                Container(
                  width: double.infinity,
                  padding: const EdgeInsets.all(16),
                  margin: const EdgeInsets.only(bottom: 16),
                  decoration: BoxDecoration(
                    color: Colors.red[50],
                    border: Border.all(color: Colors.red[200]!),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Row(
                    children: [
                      Icon(Icons.error, color: Colors.red[700]),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          _errorMessage!,
                          style: TextStyle(color: Colors.red[700]),
                        ),
                      ),
                    ],
                  ),
                ),
              _buildTextField(
                controller: _nameController,
                label: 'Name',
                icon: Icons.person,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Name ist erforderlich';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              _buildTextField(
                controller: _emailController,
                label: 'E-Mail',
                icon: Icons.email,
                keyboardType: TextInputType.emailAddress,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'E-Mail ist erforderlich';
                  }
                  if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
                    return 'Ungültige E-Mail-Adresse';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              _buildTextField(
                controller: _phoneController,
                label: 'Telefon',
                icon: Icons.phone,
                keyboardType: TextInputType.phone,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Telefon ist erforderlich';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              _buildTextField(
                controller: _addressController,
                label: 'Adresse',
                icon: Icons.location_on,
                maxLines: 3,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Adresse ist erforderlich';
                  }
                  return null;
                },
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildTextField({
    required TextEditingController controller,
    required String label,
    required IconData icon,
    TextInputType? keyboardType,
    int? maxLines,
    String? Function(String?)? validator,
  }) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 8),
        TextFormField(
          controller: controller,
          keyboardType: keyboardType,
          maxLines: maxLines,
          validator: validator,
          decoration: InputDecoration(
            prefixIcon: Icon(icon),
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8),
            ),
            focusedBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8),
              borderSide: BorderSide(color: Colors.blue),
            ),
            errorBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8),
              borderSide: BorderSide(color: Colors.red),
            ),
          ),
        ),
      ],
    );
  }

  Future<void> _saveUser() async {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final user = User(
        id: widget.user?.id ?? DateTime.now().millisecondsSinceEpoch().toString(),
        name: _nameController.text,
        email: _emailController.text,
        phone: _phoneController.text,
        address: _addressController.text,
        createdAt: widget.user?.createdAt ?? DateTime.now(),
        updatedAt: DateTime.now(),
      );

      final store = StoreProvider.of<AppState>(context);

      if (widget.user == null) {
        store.dispatch(CreateUsersAction(user));
      } else {
        store.dispatch(UpdateUsersAction(user));
      }

      Navigator.pop(context);
    } catch (e) {
      setState(() {
        _errorMessage = 'Ein Fehler ist aufgetreten: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
}

3. Progressive Web App (PWA)

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mobile PWA App</title>
    
    <!-- PWA Meta Tags -->
    <meta name="theme-color" content="#2E86AB">
    <meta name="description" content="Progressive Web App für mobile Benutzer">
    
    <!-- Apple Web App Meta Tags -->
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="default">
    <meta name="apple-mobile-web-app-title" content="Mobile PWA">
    <link rel="apple-touch-icon" href="icons/icon-192x192.png">
    
    <!-- Manifest -->
    <link rel="manifest" href="manifest.json">
    
    <!-- Service Worker Registration -->
    <script>
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('/sw.js')
                    .then(registration => {
                        console.log('SW registered: ', registration);
                    })
                    .catch(registrationError => {
                        console.log('SW registration failed: ', registrationError);
                    });
            });
        }
    </script>
    
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    
    <!-- Custom Styles -->
    <style>
        :root {
            --primary-color: #2E86AB;
            --secondary-color: #A23B72;
            --accent-color: #F18F01;
            --success-color: #C73E1D;
            --warning-color: #F4A261;
            --text-color: #333333;
            --background-color: #f8f9fa;
            --card-background: #ffffff;
            --border-color: #e0e0e0;
        }
        
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background-color: var(--background-color);
            color: var(--text-color);
            line-height: 1.6;
        }
        
        .container {
            max-width: 100%;
            padding: 0 16px;
            margin: 0 auto;
        }
        
        @media (min-width: 640px) {
            .container {
                max-width: 640px;
            }
        }
        
        @media (min-width: 768px) {
            .container {
                max-width: 768px;
            }
        }
        
        @media (min-width: 1024px) {
            .container {
                max-width: 1024px;
            }
        }
        
        .card {
            background: var(--card-background);
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            padding: 16px;
            margin-bottom: 16px;
            transition: transform 0.2s ease, box-shadow 0.2s ease;
        }
        
        .card:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 16px rgba(0,0,0,0.15);
        }
        
        .btn {
            padding: 12px 24px;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s ease;
            text-decoration: none;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
        }
        
        .btn-primary {
            background: var(--primary-color);
            color: white;
        }
        
        .btn-primary:hover {
            background: #2563eb;
            transform: translateY(-1px);
        }
        
        .btn-secondary {
            background: var(--secondary-color);
            color: white;
        }
        
        .btn-secondary:hover {
            background: #8b2b5e;
            transform: translateY(-1px);
        }
        
        .btn-outline {
            background: transparent;
            border: 2px solid var(--primary-color);
            color: var(--primary-color);
        }
        
        .btn-outline:hover {
            background: var(--primary-color);
            color: white;
        }
        
        .form-input {
            width: 100%;
            padding: 12px 16px;
            border: 2px solid var(--border-color);
            border-radius: 8px;
            font-size: 16px;
            transition: border-color 0.2s ease;
        }
        
        .form-input:focus {
            outline: none;
            border-color: var(--primary-color);
        }
        
        .form-input.error {
            border-color: var(--success-color);
        }
        
        .error-message {
            color: var(--success-color);
            font-size: 14px;
            margin-top: 4px;
        }
        
        .loading {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            color: var(--primary-color);
        }
        
        .spinner {
            width: 20px;
            height: 20px;
            border: 2px solid var(--border-color);
            border-top: 2px solid var(--primary-color);
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        .toast {
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: var(--text-color);
            color: white;
            padding: 12px 24px;
            border-radius: 8px;
            box-shadow: 0 4px 16px rgba(0,0,0,0.2);
            z-index: 1000;
            animation: slideUp 0.3s ease;
        }
        
        @keyframes slideUp {
            from {
                transform: translateX(-50%) translateY(100%);
                opacity: 0;
            }
            to {
                transform: translateX(-50%) translateY(0);
                opacity: 1;
            }
        }
        
        .modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 1000;
        }
        
        .modal-content {
            background: var(--card-background);
            border-radius: 12px;
            padding: 24px;
            max-width: 500px;
            width: 90%;
            max-height: 90vh;
            overflow-y: auto;
        }
        
        .offline-indicator {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            background: var(--warning-color);
            color: white;
            text-align: center;
            padding: 8px;
            z-index: 1001;
        }
        
        .pull-to-refresh {
            position: relative;
            padding-top: 60px;
            transition: padding-top 0.3s ease;
        }
        
        .pull-to-refresh.refreshing {
            padding-top: 80px;
        }
        
        .pull-to-refresh-indicator {
            position: absolute;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            align-items: center;
            gap: 8px;
            color: var(--primary-color);
        }
    </style>
</head>
<body>
    <!-- Header -->
    <header class="bg-white shadow-md sticky top-0 z-50">
        <div class="container">
            <div class="flex items-center justify-between h-16">
                <div class="flex items-center gap-3">
                    <div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center">
                        <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
                        </svg>
                    </div>
                    <h1 class="text-xl font-bold text-gray-900">Mobile PWA</h1>
                </div>
                <div class="flex items-center gap-2">
                    <button id="syncBtn" class="p-2 rounded-lg hover:bg-gray-100">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
                        </svg>
                    </button>
                    <button id="menuBtn" class="p-2 rounded-lg hover:bg-gray-100">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
                        </svg>
                    </button>
                </div>
            </div>
        </div>
    </header>

    <!-- Main Content -->
    <main class="container py-6">
        <!-- Stats Section -->
        <section class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
            <div class="card text-center">
                <div class="text-2xl font-bold text-blue-500" id="totalUsers">0</div>
                <div class="text-sm text-gray-600">Benutzer</div>
            </div>
            <div class="card text-center">
                <div class="text-2xl font-bold text-green-500" id="activeUsers">0</div>
                <div class="text-sm text-gray-600">Aktiv</div>
            </div>
            <div class="card text-center">
                <div class="text-2xl font-bold text-purple-500" id="totalOrders">0</div>
                <div class="text-sm text-gray-600">Bestellungen</div>
            </div>
            <div class="card text-center">
                <div class="text-2xl font-bold text-orange-500" id="revenue">€0</div>
                <div class="text-sm text-gray-600">Umsatz</div>
            </div>
        </section>

        <!-- Actions Section -->
        <section class="mb-6">
            <div class="flex flex-wrap gap-2">
                <button id="addUserBtn" class="btn btn-primary">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
                    </svg>
                    Benutzer hinzufügen
                </button>
                <button id="filterBtn" class="btn btn-outline">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.293H3a1 1 0 01-1-1V4z"/>
                    </svg>
                    Filter
                </button>
                <button id="sortBtn" class="btn btn-outline">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4h12"/>
                    </svg>
                    Sortieren
                </button>
            </div>
        </section>

        <!-- Search Section -->
        <section class="mb-6">
            <div class="relative">
                <input 
                    type="text" 
                    id="searchInput"
                    placeholder="Benutzer suchen..."
                    class="form-input pl-10"
                >
                <svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
                </svg>
            </div>
        </section>

        <!-- Users List -->
        <section id="usersList" class="pull-to-refresh">
            <div class="pull-to-refresh-indicator" style="display: none;">
                <div class="spinner"></div>
                <span>Lade Benutzer...</span>
            </div>
            <div id="usersContainer">
                <!-- Users will be dynamically added here -->
            </div>
        </section>
    </main>

    <!-- User Form Modal -->
    <div id="userModal" class="modal" style="display: none;">
        <div class="modal-content">
            <div class="flex items-center justify-between mb-6">
                <h2 class="text-xl font-bold">Benutzer hinzufügen</h2>
                <button id="closeModalBtn" class="p-2 rounded-lg hover:bg-gray-100">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                    </svg>
                </button>
            </div>
            
            <form id="userForm">
                <div class="mb-4">
                    <label class="block text-sm font-medium mb-2">Name *</label>
                    <input type="text" id="userName" class="form-input" required>
                    <div class="error-message" id="nameError"></div>
                </div>
                
                <div class="mb-4">
                    <label class="block text-sm font-medium mb-2">E-Mail *</label>
                    <input type="email" id="userEmail" class="form-input" required>
                    <div class="error-message" id="emailError"></div>
                </div>
                
                <div class="mb-4">
                    <label class="block text-sm font-medium mb-2">Telefon *</label>
                    <input type="tel" id="userPhone" class="form-input" required>
                    <div class="error-message" id="phoneError"></div>
                </div>
                
                <div class="mb-6">
                    <label class="block text-sm font-medium mb-2">Adresse *</label>
                    <textarea id="userAddress" class="form-input" rows="3" required></textarea>
                    <div class="error-message" id="addressError"></div>
                </div>
                
                <div class="flex gap-2 justify-end">
                    <button type="button" id="cancelBtn" class="btn btn-outline">Abbrechen</button>
                    <button type="submit" id="saveBtn" class="btn btn-primary">
                        <span id="saveBtnText">Speichern</span>
                        <div class="spinner" style="display: none;"></div>
                    </button>
                </div>
            </form>
        </div>
    </div>

    <!-- Toast Container -->
    <div id="toastContainer"></div>

    <!-- JavaScript -->
    <script>
        // App State
        const app = {
            users: [],
            isLoading: false,
            isOnline: navigator.onLine,
            editingUser: null,
            
            // Initialize app
            init() {
                this.bindEvents();
                this.checkOnlineStatus();
                this.loadUsers();
                this.updateStats();
            },
            
            // Event binding
            bindEvents() {
                // Header buttons
                document.getElementById('syncBtn').addEventListener('click', () => this.syncData());
                document.getElementById('menuBtn').addEventListener('click', () => this.toggleMenu());
                
                // Action buttons
                document.getElementById('addUserBtn').addEventListener('click', () => this.showUserModal());
                document.getElementById('filterBtn').addEventListener('click', () => this.showFilterOptions());
                document.getElementById('sortBtn').addEventListener('click', () => this.showSortOptions());
                
                // Search
                document.getElementById('searchInput').addEventListener('input', (e) => this.searchUsers(e.target.value));
                
                // Modal
                document.getElementById('closeModalBtn').addEventListener('click', () => this.hideUserModal());
                document.getElementById('cancelBtn').addEventListener('click', () => this.hideUserModal());
                document.getElementById('userForm').addEventListener('submit', (e) => this.saveUser(e));
                
                // Pull to refresh
                this.initPullToRefresh();
                
                // Online status
                window.addEventListener('online', () => this.checkOnlineStatus());
                window.addEventListener('offline', () => this.checkOnlineStatus());
            },
            
            // User Management
            async loadUsers() {
                this.setLoading(true);
                try {
                    if (this.isOnline) {
                        // Try to fetch from server
                        const users = await this.fetchUsersFromServer();
                        this.users = users;
                        this.saveUsersToCache(users);
                    } else {
                        // Load from cache
                        const users = await this.getUsersFromCache();
                        this.users = users;
                    }
                    this.renderUsers();
                } catch (error) {
                    console.error('Error loading users:', error);
                    this.showToast('Fehler beim Laden der Benutzer', 'error');
                } finally {
                    this.setLoading(false);
                }
            },
            
            async fetchUsersFromServer() {
                // Simulate API call
                return new Promise((resolve) => {
                    setTimeout(() => {
                        resolve([
                            {
                                id: '1',
                                name: 'Max Mustermann',
                                email: 'max@example.com',
                                phone: '+49 123 456789',
                                address: 'Musterstraße 1, 12345 Berlin',
                                status: 'active',
                                createdAt: new Date().toISOString(),
                                updatedAt: new Date().toISOString()
                            },
                            {
                                id: '2',
                                name: 'Erika Mustermann',
                                email: 'erika@example.com',
                                phone: '+49 987 654321',
                                address: 'Beispielweg 2, 54321 Hamburg',
                                status: 'active',
                                createdAt: new Date().toISOString(),
                                updatedAt: new Date().toISOString()
                            }
                        ]);
                    }, 1000);
                });
            },
            
            async saveUsersToCache(users) {
                try {
                    const cache = await caches.open('users-cache-v1');
                    const response = new Response(JSON.stringify(users));
                    await cache.put('/users', response);
                } catch (error) {
                    console.error('Error saving to cache:', error);
                }
            },
            
            async getUsersFromCache() {
                try {
                    const cache = await caches.open('users-cache-v1');
                    const response = await cache.match('/users');
                    if (response) {
                        return await response.json();
                    }
                } catch (error) {
                    console.error('Error loading from cache:', error);
                }
                return [];
            },
            
            renderUsers() {
                const container = document.getElementById('usersContainer');
                
                if (this.users.length === 0) {
                    container.innerHTML = `
                        <div class="card text-center py-12">
                            <svg class="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
                            </svg>
                            <p class="text-gray-600">Keine Benutzer gefunden</p>
                            <button class="btn btn-primary mt-4" onclick="app.showUserModal()">
                                Ersten Benutzer hinzufügen
                            </button>
                        </div>
                    `;
                    return;
                }
                
                container.innerHTML = this.users.map(user => `
                    <div class="card">
                        <div class="flex items-start justify-between">
                            <div class="flex items-center gap-3">
                                <div class="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center">
                                    <span class="text-white font-bold">${user.name.charAt(0).toUpperCase()}</span>
                                </div>
                                <div>
                                    <h3 class="font-semibold">${user.name}</h3>
                                    <p class="text-sm text-gray-600">${user.email}</p>
                                    <p class="text-sm text-gray-600">${user.phone}</p>
                                    <p class="text-sm text-gray-600">${user.address}</p>
                                </div>
                            </div>
                            <div class="flex gap-2">
                                <button class="btn btn-outline" onclick="app.editUser('${user.id}')">
                                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-4h-4l-2.828 2.828z"/>
                                    </svg>
                                </button>
                                <button class="btn btn-outline" onclick="app.deleteUser('${user.id}')">
                                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
                                    </svg>
                                </button>
                            </div>
                        </div>
                    </div>
                `).join('');
            },
            
            async saveUser(event) {
                event.preventDefault();
                
                const formData = {
                    name: document.getElementById('userName').value,
                    email: document.getElementById('userEmail').value,
                    phone: document.getElementById('userPhone').value,
                    address: document.getElementById('userAddress').value
                };
                
                if (!this.validateUserForm(formData)) {
                    return;
                }
                
                this.setLoading(true, true);
                
                try {
                    if (this.editingUser) {
                        // Update existing user
                        const index = this.users.findIndex(u => u.id === this.editingUser);
                        this.users[index] = {
                            ...this.users[index],
                            ...formData,
                            updatedAt: new Date().toISOString()
                        };
                        this.showToast('Benutzer aktualisiert', 'success');
                    } else {
                        // Create new user
                        const newUser = {
                            id: Date.now().toString(),
                            ...formData,
                            status: 'active',
                            createdAt: new Date().toISOString(),
                            updatedAt: new Date().toISOString()
                        };
                        this.users.push(newUser);
                        this.showToast('Benutzer erstellt', 'success');
                    }
                    
                    await this.saveUsersToCache(this.users);
                    this.renderUsers();
                    this.updateStats();
                    this.hideUserModal();
                    
                    if (this.isOnline) {
                        // Sync with server
                        this.syncData();
                    }
                    
                } catch (error) {
                    console.error('Error saving user:', error);
                    this.showToast('Fehler beim Speichern', 'error');
                } finally {
                    this.setLoading(false, true);
                }
            },
            
            validateUserForm(data) {
                let isValid = true;
                
                // Clear previous errors
                document.querySelectorAll('.error-message').forEach(el => el.textContent = '');
                document.querySelectorAll('.form-input').forEach(el => el.classList.remove('error'));
                
                // Validate name
                if (!data.name.trim()) {
                    document.getElementById('nameError').textContent = 'Name ist erforderlich';
                    document.getElementById('userName').classList.add('error');
                    isValid = false;
                }
                
                // Validate email
                if (!data.email.trim()) {
                    document.getElementById('emailError').textContent = 'E-Mail ist erforderlich';
                    document.getElementById('userEmail').classList.add('error');
                    isValid = false;
                } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
                    document.getElementById('emailError').textContent = 'Ungültige E-Mail-Adresse';
                    document.getElementById('userEmail').classList.add('error');
                    isValid = false;
                }
                
                // Validate phone
                if (!data.phone.trim()) {
                    document.getElementById('phoneError').textContent = 'Telefon ist erforderlich';
                    document.getElementById('userPhone').classList.add('error');
                    isValid = false;
                }
                
                // Validate address
                if (!data.address.trim()) {
                    document.getElementById('addressError').textContent = 'Adresse ist erforderlich';
                    document.getElementById('userAddress').classList.add('error');
                    isValid = false;
                }
                
                return isValid;
            },
            
            editUser(userId) {
                const user = this.users.find(u => u.id === userId);
                if (user) {
                    this.editingUser = userId;
                    document.getElementById('userName').value = user.name;
                    document.getElementById('userEmail').value = user.email;
                    document.getElementById('userPhone').value = user.phone;
                    document.getElementById('userAddress').value = user.address;
                    document.querySelector('#userModal h2').textContent = 'Benutzer bearbeiten';
                    this.showUserModal();
                }
            },
            
            async deleteUser(userId) {
                if (!confirm('Möchten Sie diesen Benutzer wirklich löschen?')) {
                    return;
                }
                
                try {
                    this.users = this.users.filter(u => u.id !== userId);
                    await this.saveUsersToCache(this.users);
                    this.renderUsers();
                    this.updateStats();
                    this.showToast('Benutzer gelöscht', 'success');
                    
                    if (this.isOnline) {
                        // Sync with server
                        this.syncData();
                    }
                } catch (error) {
                    console.error('Error deleting user:', error);
                    this.showToast('Fehler beim Löschen', 'error');
                }
            },
            
            searchUsers(query) {
                const filteredUsers = this.users.filter(user => 
                    user.name.toLowerCase().includes(query.toLowerCase()) ||
                    user.email.toLowerCase().includes(query.toLowerCase()) ||
                    user.phone.includes(query) ||
                    user.address.toLowerCase().includes(query.toLowerCase())
                );
                
                const container = document.getElementById('usersContainer');
                if (filteredUsers.length === 0) {
                    container.innerHTML = `
                        <div class="card text-center py-12">
                            <p class="text-gray-600">Keine Benutzer gefunden für "${query}"</p>
                        </div>
                    `;
                } else {
                    this.users = filteredUsers;
                    this.renderUsers();
                    this.users = this.users; // Restore original list
                }
            },
            
            // UI Methods
            showUserModal() {
                document.getElementById('userModal').style.display = 'flex';
                document.body.style.overflow = 'hidden';
            },
            
            hideUserModal() {
                document.getElementById('userModal').style.display = 'none';
                document.body.style.overflow = '';
                document.getElementById('userForm').reset();
                document.querySelectorAll('.error-message').forEach(el => el.textContent = '');
                document.querySelectorAll('.form-input').forEach(el => el.classList.remove('error'));
                this.editingUser = null;
                document.querySelector('#userModal h2').textContent = 'Benutzer hinzufügen';
            },
            
            setLoading(loading, button = false) {
                this.isLoading = loading;
                
                if (button) {
                    const saveBtn = document.getElementById('saveBtn');
                    const saveBtnText = document.getElementById('saveBtnText');
                    const spinner = saveBtn.querySelector('.spinner');
                    
                    if (loading) {
                        saveBtn.disabled = true;
                        saveBtnText.style.display = 'none';
                        spinner.style.display = 'block';
                    } else {
                        saveBtn.disabled = false;
                        saveBtnText.style.display = 'inline';
                        spinner.style.display = 'none';
                    }
                }
            },
            
            showToast(message, type = 'info') {
                const toast = document.createElement('div');
                toast.className = 'toast';
                toast.textContent = message;
                
                if (type === 'error') {
                    toast.style.background = '#dc2626';
                } else if (type === 'success') {
                    toast.style.background = '#16a34a';
                }
                
                document.getElementById('toastContainer').appendChild(toast);
                
                setTimeout(() => {
                    toast.remove();
                }, 3000);
            },
            
            updateStats() {
                document.getElementById('totalUsers').textContent = this.users.length;
                document.getElementById('activeUsers').textContent = this.users.filter(u => u.status === 'active').length;
                document.getElementById('totalOrders').textContent = Math.floor(Math.random() * 1000);
                document.getElementById('revenue').textContent = `€${Math.floor(Math.random() * 10000).toLocaleString()}`;
            },
            
            checkOnlineStatus() {
                this.isOnline = navigator.onLine;
                
                const indicator = document.querySelector('.offline-indicator');
                if (!this.isOnline && !indicator) {
                    const offlineDiv = document.createElement('div');
                    offlineDiv.className = 'offline-indicator';
                    offlineDiv.textContent = 'Offline - Arbeiten mit lokalen Daten';
                    document.body.insertBefore(offlineDiv, document.body.firstChild);
                } else if (this.isOnline && indicator) {
                    indicator.remove();
                }
                
                // Update sync button
                const syncBtn = document.getElementById('syncBtn');
                if (this.isOnline) {
                    syncBtn.style.opacity = '1';
                } else {
                    syncBtn.style.opacity = '0.5';
                }
            },
            
            async syncData() {
                if (!this.isOnline) {
                    this.showToast('Offline - Synchronisierung nicht möglich', 'error');
                    return;
                }
                
                const syncBtn = document.getElementById('syncBtn');
                const spinner = syncBtn.querySelector('.spinner');
                
                syncBtn.style.opacity = '0.5';
                if (!spinner) {
                    const spin = document.createElement('div');
                    spin.className = 'spinner';
                    spin.style.width = '16px';
                    spin.style.height = '16px';
                    syncBtn.appendChild(spin);
                }
                
                try {
                    // Simulate sync with server
                    await new Promise(resolve => setTimeout(resolve, 2000));
                    this.showToast('Daten synchronisiert', 'success');
                } catch (error) {
                    console.error('Sync error:', error);
                    this.showToast('Synchronisierung fehlgeschlagen', 'error');
                } finally {
                    syncBtn.style.opacity = '1';
                    if (spinner) {
                        spinner.remove();
                    }
                }
            },
            
            initPullToRefresh() {
                const pullToRefresh = document.getElementById('usersList');
                const indicator = pullToRefresh.querySelector('.pull-to-refresh-indicator');
                let startY = 0;
                let currentY = 0;
                let isPulling = false;
                
                pullToRefresh.addEventListener('touchstart', (e) => {
                    startY = e.touches[0].clientY;
                    isPulling = true;
                });
                
                pullToRefresh.addEventListener('touchmove', (e) => {
                    if (!isPulling) return;
                    
                    currentY = e.touches[0].clientY;
                    const deltaY = currentY - startY;
                    
                    if (deltaY > 0) {
                        const pullHeight = Math.min(deltaY, 60);
                        pullToRefresh.style.paddingTop = `${60 + pullHeight}px`;
                        
                        if (pullHeight > 40) {
                            indicator.style.display = 'flex';
                        } else {
                            indicator.style.display = 'none';
                        }
                    }
                });
                
                pullToRefresh.addEventListener('touchend', () => {
                    if (!isPulling) return;
                    
                    const deltaY = currentY - startY;
                    
                    if (deltaY > 60) {
                        pullToRefresh.classList.add('refreshing');
                        indicator.style.display = 'flex';
                        indicator.innerHTML = '<div class="spinner"></div><span>Lade Daten...</span>';
                        
                        this.loadUsers().then(() => {
                            pullToRefresh.classList.remove('refreshing');
                            pullToRefresh.style.paddingTop = '60px';
                            indicator.style.display = 'none';
                        });
                    } else {
                        pullToRefresh.style.paddingTop = '60px';
                        indicator.style.display = 'none';
                    }
                    
                    isPulling = false;
                    startY = 0;
                    currentY = 0;
                });
            },
            
            toggleMenu() {
                // Mobile menu implementation
                this.showToast('Mobile Menü (noch nicht implementiert)', 'info');
            },
            
            showFilterOptions() {
                // Filter implementation
                this.showToast('Filter-Optionen (noch nicht implementiert)', 'info');
            },
            
            showSortOptions() {
                // Sort implementation
                this.showToast('Sortier-Optionen (noch nicht implementiert)', 'info');
            }
        };
        
        // Initialize app when DOM is ready
        document.addEventListener('DOMContentLoaded', () => {
            app.init();
        });
    </script>
</body>
</html>

4. Service Worker für PWA

// sw.js - Service Worker für Progressive Web App
const CACHE_NAME = 'mobile-pwa-v1';
const urlsToCache = [
    '/',
    '/index.html',
    '/manifest.json',
    '/icons/icon-192x192.png',
    '/icons/icon-512x512.png',
    // Add other static assets here
];

// Install Event - Cache static assets
self.addEventListener('install', (event) => {
    console.log('SW: Installing...');
    
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then((cache) => {
                console.log('SW: Caching static assets');
                return cache.addAll(urlsToCache);
            })
            .then(() => {
                console.log('SW: Static assets cached successfully');
                return self.skipWaiting();
            })
            .catch((error) => {
                console.error('SW: Failed to cache static assets:', error);
            })
    );
});

// Activate Event - Clean up old caches
self.addEventListener('activate', (event) => {
    console.log('SW: Activating...');
    
    event.waitUntil(
        caches.keys()
            .then((cacheNames) => {
                return Promise.all(
                    cacheNames.map((cacheName) => {
                        if (cacheName !== CACHE_NAME) {
                            console.log('SW: Deleting old cache:', cacheName);
                            return caches.delete(cacheName);
                        }
                    })
                );
            })
            .then(() => {
                console.log('SW: Old caches deleted successfully');
                return self.clients.claim();
            })
            .catch((error) => {
                console.error('SW: Failed to delete old caches:', error);
            })
    );
});

// Fetch Event - Network-first with cache fallback
self.addEventListener('fetch', (event) => {
    const request = event.request;
    const url = new URL(request.url);
    
    // Skip non-GET requests and external requests
    if (request.method !== 'GET' || url.origin !== self.location.origin) {
        return;
    }
    
    event.respondWith(
        caches.match(request)
            .then((response) => {
                // Return cached version if available
                if (response) {
                    console.log('SW: Serving from cache:', request.url);
                    return response;
                }
                
                // Otherwise fetch from network
                console.log('SW: Fetching from network:', request.url);
                return fetch(request)
                    .then((response) => {
                        // Cache successful responses
                        if (response.status === 200) {
                            const responseClone = response.clone();
                            caches.open(CACHE_NAME)
                                .then((cache) => {
                                    console.log('SW: Caching new response:', request.url);
                                    cache.put(request, responseClone);
                                })
                                .catch((error) => {
                                    console.error('SW: Failed to cache response:', error);
                                });
                        }
                        return response;
                    })
                    .catch((error) => {
                        console.error('SW: Network request failed:', error);
                        
                        // Try to serve from cache if network fails
                        return caches.match(request)
                            .then((cachedResponse) => {
                                if (cachedResponse) {
                                    console.log('SW: Network failed, serving from cache:', request.url);
                                    return cachedResponse;
                                }
                                
                                // Return offline page if nothing in cache
                                return caches.match('/offline.html');
                            });
                    });
            })
    );
});

// Background Sync for offline actions
self.addEventListener('sync', (event) => {
    console.log('SW: Background sync triggered:', event.tag);
    
    if (event.tag === 'background-sync-users') {
        event.waitUntil(
            syncUsersData()
                .then(() => {
                    console.log('SW: Background sync completed');
                })
                .catch((error) => {
                    console.error('SW: Background sync failed:', error);
                })
        );
    }
});

// Push Notifications
self.addEventListener('push', (event) => {
    console.log('SW: Push message received');
    
    const options = {
        body: event.data.text(),
        icon: '/icons/icon-192x192.png',
        badge: '/icons/icon-192x192.png',
        vibrate: [100, 50, 100],
        data: {
            dateOfArrival: Date.now(),
            primaryKey: 1
        },
        actions: [
            {
                action: 'explore',
                title: 'Explore',
                icon: '/images/checkmark.png'
            },
            {
                action: 'close',
                title: 'Close',
                icon: '/images/xmark.png'
            }
        ]
    };
    
    event.waitUntil(
        self.registration.showNotification('Mobile PWA', options)
    );
});

// Notification Click Handling
self.addEventListener('notificationclick', (event) => {
    console.log('SW: Notification clicked');
    
    event.notification.close();
    
    if (event.action === 'explore') {
        event.waitUntil(
            clients.openWindow('/')
        );
    }
});

// Message Handling
self.addEventListener('message', (event) => {
    console.log('SW: Message received:', event.data);
    
    if (event.data && event.data.type === 'SKIP_WAITING') {
        self.skipWaiting();
    }
});

// Helper Functions
async function syncUsersData() {
    try {
        // Get all pending user actions from IndexedDB
        const pendingActions = await getPendingActions();
        
        for (const action of pendingActions) {
            try {
                // Sync with server
                await syncActionWithServer(action);
                
                // Remove from pending actions
                await removePendingAction(action.id);
                
                // Notify client about successful sync
                notifyClient('sync-success', { actionId: action.id });
            } catch (error) {
                console.error('SW: Failed to sync action:', action, error);
                notifyClient('sync-error', { actionId: action.id, error: error.message });
            }
        }
    } catch (error) {
        console.error('SW: Background sync failed:', error);
        throw error;
    }
}

async function getPendingActions() {
    // IndexedDB implementation for pending actions
    return new Promise((resolve, reject) => {
        const request = indexedDB.open('offline-actions', 1);
        
        request.onerror = () => reject(request.error);
        request.onsuccess = () => {
            const db = request.result;
            const transaction = db.transaction(['actions'], 'readonly');
            const store = transaction.objectStore('actions');
            const getRequest = store.getAll();
            
            getRequest.onerror = () => reject(getRequest.error);
            getRequest.onsuccess = () => resolve(getRequest.result);
        };
    });
}

async function syncActionWithServer(action) {
    const response = await fetch('/api/sync', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(action)
    });
    
    if (!response.ok) {
        throw new Error(`Sync failed: ${response.status}`);
    }
    
    return response.json();
}

async function removePendingAction(actionId) {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open('offline-actions', 1);
        
        request.onerror = () => reject(request.error);
        request.onsuccess = () => {
            const db = request.result;
            const transaction = db.transaction(['actions'], 'readwrite');
            const store = transaction.objectStore('actions');
            const deleteRequest = store.delete(actionId);
            
            deleteRequest.onerror = () => reject(deleteRequest.error);
            deleteRequest.onsuccess = () => resolve();
        };
    });
}

function notifyClient(type, data) {
    return self.clients.matchAll().then(clients => {
        clients.forEach(client => {
            client.postMessage({ type, data });
        });
    });
}

// Cache Strategy Helpers
const cacheStrategies = {
    // Network first, falling back to cache
    networkFirst: (request) => {
        return fetch(request)
            .then(response => {
                // Cache successful responses
                if (response.ok) {
                    const responseClone = response.clone();
                    caches.open(CACHE_NAME)
                        .then(cache => cache.put(request, responseClone));
                }
                return response;
            })
            .catch(() => caches.match(request));
    },
    
    // Cache first, falling back to network
    cacheFirst: (request) => {
        return caches.match(request)
            .then(response => {
                if (response) {
                    return response;
                }
                
                // Fetch from network and cache
                return fetch(request)
                    .then(response => {
                        if (response.ok) {
                            const responseClone = response.clone();
                            caches.open(CACHE_NAME)
                                .then(cache => cache.put(request, responseClone));
                        }
                        return response;
                    });
            });
    },
    
    // Stale while revalidate
    staleWhileRevalidate: (request) => {
        return caches.match(request)
            .then(response => {
                const fetchPromise = fetch(request)
                    .then(networkResponse => {
                        if (networkResponse.ok) {
                            const networkResponseClone = networkResponse.clone();
                            caches.open(CACHE_NAME)
                                .then(cache => cache.put(request, networkResponseClone));
                        }
                        return networkResponse;
                    });
                
                return response || fetchPromise;
            });
    },
    
    // Network only
    networkOnly: (request) => {
        return fetch(request);
    },
    
    // Cache only
    cacheOnly: (request) => {
        return caches.match(request);
    }
};

// Route-specific cache strategies
const router = {
    register: (routes, strategy) => {
        routes.forEach(route => {
            if (typeof route === 'string') {
                self.addEventListener('fetch', event => {
                    if (event.request.url === route) {
                        event.respondWith(strategy(event.request));
                    }
                });
            } else if (route instanceof RegExp) {
                self.addEventListener('fetch', event => {
                    if (route.test(event.request.url)) {
                        event.respondWith(strategy(event.request));
                    }
                });
            }
        });
    }
};

// Register routes with specific strategies
router.register([
    '/api/users',
    /^\/api\/users\/\d+$/
], cacheStrategies.networkFirst);

router.register([
    '/static/',
    /\.(js|css|png|jpg|jpeg|svg|gif)$/
], cacheStrategies.cacheFirst);

router.register([
    '/'
], cacheStrategies.staleWhileRevalidate);

console.log('SW: Service Worker loaded successfully');

Mobile Entwicklungsansätze Vergleich

KriteriumNative iOS (Swift)Native Android (Kotlin)React NativeFlutterPWA
PerformanceOptimalOptimalGutSehr GutGut
UI/UXPlattform-StandardPlattform-StandardCross-PlatformCross-PlatformWeb-Standard
EntwicklungPlattform-spezifischPlattform-spezifischJavaScriptDartWeb-Technologien
Code-WiederverwendungKeineKeine90%+95%+100%
Hardware-ZugriffVollVollBegrenztBegrenztSehr begrenzt
App StoreJaJaJaJaNein
Offline-FähigkeitJaJaJaJaJa
Push-NotificationsJaJaJaJaJa
Update-ProzessApp StorePlay StoreOver-the-AirOver-the-AirAutomatisch

Cross-Platform Frameworks Vergleich

React Native vs. Flutter

FeatureReact NativeFlutter
SpracheJavaScript/TypeScriptDart
UI-RenderingNative ComponentsCustom Engine
PerformanceGutSehr Gut
Hot ReloadJaJa
CommunityGroßWachsend
Learning CurveMittelMittel
EcosystemReactGoogle

Weitere Cross-Platform Lösungen

  • Xamarin: C# mit .NET (Microsoft)
  • Ionic: Web-Technologien mit Capacitor
  • Cordova: Web-View Wrapper
  • Qt: C++ Framework
  • Unity: C# für 3D/Games

PWA Features und Capabilities

Core PWA Features

// Service Worker Registration
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
        .then(registration => console.log('SW registered'))
        .catch(error => console.log('SW registration failed'));
}

// Install Prompt
window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    showInstallButton();
});

// Background Sync
if ('serviceWorker' in navigator && 'SyncManager' in window) {
    registration.sync.register('background-sync');
}

PWA Limitations

  • iOS Safari: Begrenzte Unterstützung
  • Hardware-Zugriff: Kamera, GPS mit Einschränkungen
  • Performance: Langsamer als Native Apps
  • Speicher: Begrenzte Offline-Kapazität
  • Push-Notifications: Nicht auf allen Plattformen

Mobile UI/UX Best Practices

Responsive Design

/* Mobile-first responsive design */
.container {
    max-width: 100%;
    padding: 0 16px;
    margin: 0 auto;
}

@media (min-width: 768px) {
    .container {
        max-width: 768px;
    }
}

/* Touch-friendly buttons */
.btn {
    min-height: 44px;
    min-width: 44px;
    padding: 12px 16px;
}

Performance Optimization

  • Lazy Loading: Bilder und Komponenten
  • Code Splitting: Dynamische Imports
  • Bundle Size: Minimierung und Kompression
  • Image Optimization: WebP, Responsive Images
  • Caching: Service Worker und HTTP-Caching

Mobile Testing Strategien

Test-Automatisierung

// React Native Testing
import { render, fireEvent } from '@testing-library/react-native';
import App from './App';

test('renders correctly', () => {
    const { getByText } = render(<App />);
    expect(getByText('Welcome')).toBeTruthy();
});

// Flutter Testing
import 'package:flutter_test.dart';

void main() {
    testWidgetsTest((WidgetTester tester) async {
        await tester.pumpWidget(MyApp());
        expect(find.text('Welcome'), findsOneWidget);
    });
}

Device Testing

  • Emulatoren: iOS Simulator, Android Emulator
  • Real Devices: Verschiedene Geräte und OS-Versionen
  • Cloud Testing: BrowserStack, Sauce Labs
  • Beta Testing: TestFlight, Google Play Beta

Mobile Security Best Practices

Data Protection

// Secure Storage
import { SecureStore } from 'expo-secure-store';

// Store sensitive data securely
await SecureStore.setItemAsync('userToken', token, {
    keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});

// Encryption in Transit
const https = require('https');
const crypto = require('crypto');

// Encrypt data before storage
function encryptData(data) {
    const algorithm = 'aes-256-gcm';
    const key = crypto.randomBytes(32);
    const iv = crypto.randomBytes(16);
    
    const cipher = crypto.createCipher(algorithm, key, iv);
    let encrypted = cipher.update(data, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    
    return {
        encrypted,
        key: key.toString('hex'),
        iv: iv.toString('hex')
    };
}

Authentication

  • Biometric: Touch ID, Face ID
  • OAuth 2.0: Secure authentication flows
  • JWT: Token-based authentication
  • 2FA: Two-factor authentication
  • Session Management: Secure session handling

Mobile Performance Optimization

Performance Metrics

// Performance Monitoring
import { performance } from 'perf_hooks';

// Measure render performance
const start = performance.now();
// ... render component
const end = performance.now();
console.log(`Render took ${end - start} milliseconds`);

// Memory monitoring
const memoryInfo = performance.memory;
console.log('Memory usage:', memoryInfo);

Optimization Techniques

  • Virtual Lists: Für große Datenmengen
  • Image Optimization: Lazy Loading, WebP
  • Bundle Optimization: Tree Shaking, Code Splitting
  • Memory Management: Leak prevention
  • Network Optimization: Request batching, caching

Vorteile und Nachteile

Vorteile von Cross-Platform

  • Cost-Efficiency: Ein Codebase für alle Plattformen
  • Faster Development: Schnellere Markteinführung
  • Consistency: Einheitliche User Experience
  • Maintenance: Einfachere Wartung
  • Team Skills: JavaScript/Dart-Entwickler

Nachteile

  • Performance: Nicht immer optimal
  • Platform Limitations: Einschränkter API-Zugriff
  • Debugging: Komplexere Fehleranalyse
  • Update Delays: Abhängig von Framework-Updates
  • Size: Größere App-Bundles

Häufige Prüfungsfragen

  1. Wann wählt man Native vs. Cross-Platform? Native bei hoher Performance-Anforderung, Cross-Platform bei schnellen Markteinführung und begrenztem Budget.

  2. Was sind die Hauptvorteile von PWAs? Keine App-Store-Abhängigkeit, automatische Updates, Web-basiert, offline-fähig, kostengünstig.

  3. Erklären Sie Service Worker! Service Worker sind JavaScript-Skripte, die im Hintergrund laufen und Offline-Fähigkeiten, Caching und Push-Notifications ermöglichen.

  4. Wie optimiert man Mobile App Performance? Lazy Loading, Bundle-Optimierung, Memory Management, Native-Module für kritische Pfade, Performance-Monitoring.

Wichtigste Quellen

  1. https://reactnative.dev/
  2. https://flutter.dev/
  3. https://developer.apple.com/swift/
  4. https://developer.android.com/kotlin/
Zurück zum Blog
Share: