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

Mobile App Development: React Native, Flutter, Swift, Kotlin & PWA

Compare React Native, Flutter, Swift, Kotlin, and PWA for cross-platform mobile development. Native vs. hybrid strategies, performance, and best practices.

S

schutzgeist

2 min read
Mobile App Development: React Native, Flutter, Swift, Kotlin & PWA

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

  1. Platform selection: Native vs. cross-platform vs. PWA
  2. Development tools: IDEs, SDKs, frameworks
  3. UI components: Native controls vs. custom components
  4. State management: Data flow and state management
  5. Navigation: Screen navigation and routing
  6. Performance: Optimization and memory management
  7. Testing: Unit, integration, and UI tests
  8. 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

CriterionNative iOS (Swift)Native Android (Kotlin)React NativeFlutterPWA
PerformanceOptimalOptimalGoodVery GoodGood
UI/UXPlatform-StandardPlatform-StandardCross-PlatformCross-PlatformWeb-Standard
DevelopmentPlatform-specificPlatform-specificJavaScriptDartWeb Technologies
Code ReuseNoneNone90%+95%+100%
Hardware AccessFullFullLimitedLimitedVery limited
App StoreYesYesYesYesNo
Offline CapabilityYesYesYesYesYes
Push NotificationsYesYesYesYesYes
Update ProcessApp StorePlay StoreOver-the-AirOver-the-AirAutomatic

Cross-Platform Frameworks Comparison

React Native vs. Flutter

FeatureReact NativeFlutter
LanguageJavaScript/TypeScriptDart
UI RenderingNative ComponentsCustom Engine
PerformanceGoodVery Good
Hot ReloadYesYes
CommunityLargeGrowing
Learning CurveMediumMedium
EcosystemReactGoogle

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

  1. When should you choose native vs. cross-platform? Native for high performance requirements, cross-platform for fast time-to-market and limited budgets.

  2. What are the main advantages of PWAs? No app store dependency, automatic updates, web-based, offline-capable, cost-effective.

  3. Explain Service Workers! Service Workers are JavaScript scripts that run in the background and enable offline capabilities, caching, and push notifications.

  4. How do you optimize mobile app performance? Lazy loading, bundle optimization, memory management, native modules for critical paths, performance monitoring.

Important Resources

  1. https://reactnative.dev/
  2. https://flutter.dev/
  3. https://developer.apple.com/swift/
  4. https://developer.android.com/kotlin/
Back to Blog
Share: