Mobile App Development: React Native, Flutter, Swift, Kotlin & PWA Cross-Platform
This post is a comprehensive introduction to mobile app development – including React Native, Flutter, Swift, Kotlin, and PWA with cross-platform strategies and practical examples.
In a Nutshell
Mobile app development encompasses native (iOS/Android), cross-platform (React Native, Flutter), and web apps (PWA). Native offers the best performance, cross-platform accelerates development, PWA simplifies distribution.
Concise Technical Description
Mobile app development is the creation of applications for mobile devices such as smartphones and tablets using various technological approaches.
Development approaches:
Native Development
- iOS: Swift with UIKit or SwiftUI
- Android: Kotlin with Jetpack Compose
- Advantages: Best performance, full API utilization
- Disadvantages: Platform-specific development
Cross-Platform Development
- React Native: JavaScript/React for iOS & Android
- Flutter: Dart from Google for iOS & Android
- Advantages: Code reuse, faster development
- Disadvantages: Performance overhead, limited API access
Progressive Web Apps (PWA)
- Technology: HTML5, CSS3, JavaScript with service workers
- Advantages: One codebase, app store capable, offline-capable
- Disadvantages: Limited hardware access, browser dependency
Exam-Relevant Key Points
- Mobile app development: Creation of applications for mobile devices
- Native: Platform-specific development (Swift/Kotlin)
- Cross-platform: One codebase for multiple platforms
- React Native: JavaScript-based cross-platform solution
- Flutter: Dart-based cross-platform solution from Google
- PWA: Progressive web apps with app-like behavior
- UI/UX: User interface and user experience design
- IHK-relevant: Modern mobile application development
Core Components
- Platform selection: Native vs. cross-platform vs. PWA
- Development tools: IDEs, SDKs, frameworks
- UI components: Native controls vs. custom components
- State management: Data flow and state management
- Navigation: Screen navigation and routing
- Performance: Optimization and memory management
- Testing: Unit, integration, and UI tests
- Deployment: App store and distribution
Practical Examples
1. React Native App with 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(
'Delete user',
`Do you really want to delete ${user.name}?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: () => dispatch(deleteUser(user.id)),
},
]
);
};
if (loading) {
return (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color="#2E86AB" />
<Text style={styles.loadingText}>Loading users...</Text>
</View>
);
}
if (error) {
return (
<View style={styles.centerContainer}>
<Text style={styles.errorText}>Error: {error}</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={() => dispatch(fetchUsers())}
>
<Text style={styles.retryButtonText}>Try again</Text>
</TouchableOpacity>
</View>
);
}
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
<View style={styles.header}>
<Text style={styles.headerTitle}>User list</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}>Edit</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.deleteButton]}
onPress={() => handleDeleteUser(user)}
>
<Text style={styles.actionButtonText}>Delete</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 is required';
}
if (!formData.email?.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email address';
}
if (!formData.phone?.trim()) {
newErrors.phone = 'Phone is required';
}
if (!formData.address?.trim()) {
newErrors.address = 'Address is required';
}
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('Success', 'User was created');
} else {
await dispatch(updateUser({ ...formData, id: user!.id } as User));
Alert.alert('Success', 'User was updated');
}
navigation.goBack();
} catch (error) {
Alert.alert('Error', 'An error occurred');
} 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}>Cancel</Text>
</TouchableOpacity>
<Text style={styles.formTitle}>
{mode === 'create' ? 'Create user' : 'Edit user'}
</Text>
<TouchableOpacity onPress={handleSubmit} disabled={loading}>
<Text style={[styles.saveButton, loading && styles.disabledButton]}>
{loading ? 'Saving...' : 'Save'}
</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="Enter name"
editable={!loading}
/>
{errors.name && <Text style={styles.errorText}>{errors.name}</Text>}
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Email *</Text>
<TextInput
style={[styles.input, errors.email && styles.inputError]}
value={formData.email}
onChangeText={(value) => updateField('email', value)}
placeholder="Enter email"
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}>Phone *</Text>
<TextInput
style={[styles.input, errors.phone && styles.inputError]}
value={formData.phone}
onChangeText={(value) => updateField('phone', value)}
placeholder="Enter phone"
keyboardType="phone-pad"
editable={!loading}
/>
{errors.phone && <Text style={styles.errorText}>{errors.phone}</Text>}
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Address *</Text>
<TextInput
style={[styles.input, errors.address && styles.inputError]}
value={formData.address}
onChangeText={(value) => updateField('address', value)}
placeholder="Enter address"
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: 'User App',
headerStyle: {
backgroundColor: '#2E86AB',
},
headerTintColor: '#ffffff',
}}
/>
<Stack.Screen
name="UserForm"
component={UserFormScreen}
options={{
title: 'User Form',
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 with 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('User List'),
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('Loading users...'),
],
),
);
}
if (viewModel.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text(
'Error: ${viewModel.error}',
style: TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => StoreProvider.of<AppState>(context).dispatch(FetchUsersAction()),
child: Text('Retry'),
),
],
),
);
}
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(
'No users found',
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('Edit'),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete', 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(
'Created: ${_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('Delete User'),
content: Text('Are you sure you want to delete ${user.name}?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
StoreProvider.of<AppState>(context).dispatch(DeleteUsersAction(user.id));
},
child: Text(
'Delete',
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 ? 'Create User' : 'Edit User'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
actions: [
if (!_isLoading)
TextButton(
onPressed: _saveUser,
child: Text(
'Save',
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 is required';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _emailController,
label: 'Email',
icon: Icons.email,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Invalid email address';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _phoneController,
label: 'Phone',
icon: Icons.phone,
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Phone is required';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _addressController,
label: 'Address',
icon: Icons.location_on,
maxLines: 3,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Address is required';
}
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 = 'An error occurred: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
}
3. Progressive Web App (PWA)
<!DOCTYPE html>
<html lang="en">
<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 for mobile users">
<!-- 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">Users</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">Active</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">Orders</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">Revenue</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>
Add User
</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>
Sort
</button>
</div>
</section>
<!-- Search Section -->
<section class="mb-6">
<div class="relative">
<input
type="text"
id="searchInput"
placeholder="Search users..."
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>Loading users...</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">Add User</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">Email *</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">Phone *</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">Address *</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">Cancel</button>
<button type="submit" id="saveBtn" class="btn btn-primary">
<span id="saveBtnText">Save</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('Error loading users', '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">No users found</p>
<button class="btn btn-primary mt-4" onclick="app.showUserModal()">
Add first user
</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('User updated', '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('User created', '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('Error saving', '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 is required';
document.getElementById('userName').classList.add('error');
isValid = false;
}
// Validate email
if (!data.email.trim()) {
document.getElementById('emailError').textContent = 'Email is required';
document.getElementById('userEmail').classList.add('error');
isValid = false;
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
document.getElementById('emailError').textContent = 'Invalid email address';
document.getElementById('userEmail').classList.add('error');
isValid = false;
}
// Validate phone
if (!data.phone.trim()) {
document.getElementById('phoneError').textContent = 'Phone is required';
document.getElementById('userPhone').classList.add('error');
isValid = false;
}
// Validate address
if (!data.address.trim()) {
document.getElementById('addressError').textContent = 'Address is required';
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 = 'Edit User';
this.showUserModal();
}
},
async deleteUser(userId) {
if (!confirm('Are you sure you want to delete this user?')) {
return;
}
try {
this.users = this.users.filter(u => u.id !== userId);
await this.saveUsersToCache(this.users);
this.renderUsers();
this.updateStats();
this.showToast('User deleted', 'success');
if (this.isOnline) {
// Sync with server
this.syncData();
}
} catch (error) {
console.error('Error deleting user:', error);
this.showToast('Error deleting', '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.
### 4. Service Worker für PWA
```javascript
// 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 Development Approaches Comparison
| Criterion | Native iOS (Swift) | Native Android (Kotlin) | React Native | Flutter | PWA |
|---|---|---|---|---|---|
| Performance | Optimal | Optimal | Good | Very Good | Good |
| UI/UX | Platform-Standard | Platform-Standard | Cross-Platform | Cross-Platform | Web-Standard |
| Development | Platform-specific | Platform-specific | JavaScript | Dart | Web Technologies |
| Code Reuse | None | None | 90%+ | 95%+ | 100% |
| Hardware Access | Full | Full | Limited | Limited | Very limited |
| App Store | Yes | Yes | Yes | Yes | No |
| Offline Capability | Yes | Yes | Yes | Yes | Yes |
| Push Notifications | Yes | Yes | Yes | Yes | Yes |
| Update Process | App Store | Play Store | Over-the-Air | Over-the-Air | Automatic |
Cross-Platform Frameworks Comparison
React Native vs. Flutter
| Feature | React Native | Flutter |
|---|---|---|
| Language | JavaScript/TypeScript | Dart |
| UI Rendering | Native Components | Custom Engine |
| Performance | Good | Very Good |
| Hot Reload | Yes | Yes |
| Community | Large | Growing |
| Learning Curve | Medium | Medium |
| Ecosystem | React |
Other Cross-Platform Solutions
- Xamarin: C# with .NET (Microsoft)
- Ionic: Web technologies with Capacitor
- Cordova: Web-View Wrapper
- Qt: C++ Framework
- Unity: C# for 3D/Games
PWA Features and 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: Limited support
- Hardware Access: Camera, GPS with limitations
- Performance: Slower than native apps
- Storage: Limited offline capacity
- Push Notifications: Not available on all platforms
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: Images and components
- Code Splitting: Dynamic imports
- Bundle Size: Minimization and compression
- Image Optimization: WebP, Responsive Images
- Caching: Service Worker and HTTP caching
Mobile Testing Strategies
Test Automation
// 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
- Emulators: iOS Simulator, Android Emulator
- Real Devices: Various devices and OS versions
- 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: For large datasets
- Image Optimization: Lazy Loading, WebP
- Bundle Optimization: Tree Shaking, Code Splitting
- Memory Management: Leak prevention
- Network Optimization: Request batching, caching
Advantages and Disadvantages
Advantages of Cross-Platform
- Cost-Efficiency: One codebase for all platforms
- Faster Development: Faster time-to-market
- Consistency: Unified user experience
- Maintenance: Easier maintenance
- Team Skills: JavaScript/Dart developers
Disadvantages
- Performance: Not always optimal
- Platform Limitations: Restricted API access
- Debugging: More complex error analysis
- Update Delays: Dependent on framework updates
- Size: Larger app bundles
Common Exam Questions
-
When should you choose native vs. cross-platform? Native for high performance requirements, cross-platform for fast time-to-market and limited budgets.
-
What are the main advantages of PWAs? No app store dependency, automatic updates, web-based, offline-capable, cost-effective.
-
Explain Service Workers! Service Workers are JavaScript scripts that run in the background and enable offline capabilities, caching, and push notifications.
-
How do you optimize mobile app performance? Lazy loading, bundle optimization, memory management, native modules for critical paths, performance monitoring.
Important Resources
- https://reactnative.dev/
- https://flutter.dev/
- https://developer.apple.com/swift/
- https://developer.android.com/kotlin/