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
- Plattform-Auswahl: Native vs. Cross-Platform vs. PWA
- Entwicklungs-Tools: IDEs, SDKs, Frameworks
- UI-Komponenten: Native Controls vs. Custom Components
- State Management: Datenfluss und Zustandsverwaltung
- Navigation: Screen-Navigation und Routing
- Performance: Optimierung und Memory Management
- Testing: Unit, Integration und UI-Tests
- 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
| Kriterium | Native iOS (Swift) | Native Android (Kotlin) | React Native | Flutter | PWA |
|---|---|---|---|---|---|
| Performance | Optimal | Optimal | Gut | Sehr Gut | Gut |
| UI/UX | Plattform-Standard | Plattform-Standard | Cross-Platform | Cross-Platform | Web-Standard |
| Entwicklung | Plattform-spezifisch | Plattform-spezifisch | JavaScript | Dart | Web-Technologien |
| Code-Wiederverwendung | Keine | Keine | 90%+ | 95%+ | 100% |
| Hardware-Zugriff | Voll | Voll | Begrenzt | Begrenzt | Sehr begrenzt |
| App Store | Ja | Ja | Ja | Ja | Nein |
| Offline-Fähigkeit | Ja | Ja | Ja | Ja | Ja |
| Push-Notifications | Ja | Ja | Ja | Ja | Ja |
| Update-Prozess | App Store | Play Store | Over-the-Air | Over-the-Air | Automatisch |
Cross-Platform Frameworks Vergleich
React Native vs. Flutter
| Feature | React Native | Flutter |
|---|---|---|
| Sprache | JavaScript/TypeScript | Dart |
| UI-Rendering | Native Components | Custom Engine |
| Performance | Gut | Sehr Gut |
| Hot Reload | Ja | Ja |
| Community | Groß | Wachsend |
| Learning Curve | Mittel | Mittel |
| Ecosystem | React |
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
-
Wann wählt man Native vs. Cross-Platform? Native bei hoher Performance-Anforderung, Cross-Platform bei schnellen Markteinführung und begrenztem Budget.
-
Was sind die Hauptvorteile von PWAs? Keine App-Store-Abhängigkeit, automatische Updates, Web-basiert, offline-fähig, kostengünstig.
-
Erklären Sie Service Worker! Service Worker sind JavaScript-Skripte, die im Hintergrund laufen und Offline-Fähigkeiten, Caching und Push-Notifications ermöglichen.
-
Wie optimiert man Mobile App Performance? Lazy Loading, Bundle-Optimierung, Memory Management, Native-Module für kritische Pfade, Performance-Monitoring.
Wichtigste Quellen
- https://reactnative.dev/
- https://flutter.dev/
- https://developer.apple.com/swift/
- https://developer.android.com/kotlin/