Skip to content
IRC-CodingIRC-Coding
Desarrollo de Apps MóvilesReact Native FlutterSwift KotlinPWA Cross PlatformNativo Híbrido

Desarrollo de Apps Móviles: React Native y Flutter

Guía completa de desarrollo móvil con React Native, Flutter, Swift y Kotlin. Estrategias cross-platform, nativo vs híbrido y mejores prácticas.

S

schutzgeist

33 min read
Desarrollo de Apps Móviles: React Native y Flutter

Desarrollo de Aplicaciones Móviles: React Native, Flutter, Swift, Kotlin y PWA Cross-Platform

Este artículo es una introducción completa al desarrollo de aplicaciones móviles, incluyendo React Native, Flutter, Swift, Kotlin y PWA con estrategias cross-platform y ejemplos prácticos.

En síntesis

El desarrollo de aplicaciones móviles abarca tres enfoques: nativas (iOS/Android), cross-platform (React Native, Flutter) y aplicaciones web (PWA). Las nativas ofrecen el mejor rendimiento, las cross-platform aceleran el desarrollo, y las PWA simplifican la distribución.

Descripción técnica compacta

El desarrollo de aplicaciones móviles es la creación de aplicaciones para dispositivos móviles como smartphones y tablets usando diferentes enfoques tecnológicos.

Enfoques de desarrollo:

Desarrollo Nativo

  • iOS: Swift con UIKit o SwiftUI
  • Android: Kotlin con Jetpack Compose
  • Ventajas: Mejor rendimiento, acceso completo a APIs
  • Desventajas: Desarrollo específico por plataforma

Desarrollo Cross-Platform

  • React Native: JavaScript/React para iOS y Android
  • Flutter: Dart de Google para iOS y Android
  • Ventajas: Reutilización de código, desarrollo más rápido
  • Desventajas: Sobrecarga de rendimiento, acceso limitado a APIs

Progressive Web Apps (PWA)

  • Tecnología: HTML5, CSS3, JavaScript con Service Workers
  • Ventajas: Una única codebase, compatible con app stores, funcionalidad offline
  • Desventajas: Acceso limitado a hardware, dependencia del navegador

Puntos clave para el examen

  • Desarrollo de aplicaciones móviles: Creación de aplicaciones para dispositivos móviles
  • Nativo: Desarrollo específico de plataforma (Swift/Kotlin)
  • Cross-platform: Una codebase para múltiples plataformas
  • React Native: Solución cross-platform basada en JavaScript
  • Flutter: Solución cross-platform basada en Dart de Google
  • PWA: Progressive Web Apps con comportamiento similar al de una app
  • UI/UX: Diseño de interfaz y experiencia de usuario
  • Relevante para IHK: Desarrollo moderno de aplicaciones móviles

Componentes principales

  1. Selección de plataforma: Nativo vs. Cross-platform vs. PWA
  2. Herramientas de desarrollo: IDEs, SDKs, frameworks
  3. Componentes UI: Controls nativos vs. componentes personalizados
  4. Gestión de estado: Flujo de datos y administración de estado
  5. Navegación: Navegación entre pantallas y routing
  6. Rendimiento: Optimización y gestión de memoria
  7. Testing: Tests unitarios, de integración y de UI
  8. Deployment: App Store y distribución

Ejemplos prácticos

1. Aplicación React Native con TypeScript

// App.tsx - Aplicación principal
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';

// Definir tipos
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;
  };
}

// Componente Lista de Usuarios
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(
      'Eliminar usuario',
      `¿Deseas eliminar a ${user.name}?`,
      [
        { text: 'Cancelar', style: 'cancel' },
        {
          text: 'Eliminar',
          style: 'destructive',
          onPress: () => dispatch(deleteUser(user.id)),
        },
      ]
    );
  };

  if (loading) {
    return (
      <View style={styles.centerContainer}>
        <ActivityIndicator size="large" color="#2E86AB" />
        <Text style={styles.loadingText}>Cargando usuarios...</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}>Reintentar</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
      
      <View style={styles.header}>
        <Text style={styles.headerTitle}>Lista de usuarios</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}>Editar</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={[styles.actionButton, styles.deleteButton]}
                onPress={() => handleDeleteUser(user)}
              >
                <Text style={styles.actionButtonText}>Eliminar</Text>
              </TouchableOpacity>
            </View>
          </View>
        ))}
      </ScrollView>
    </SafeAreaView>
  );
};

// Componente Formulario de Usuario
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 = 'El nombre es obligatorio';
    }

    if (!formData.email?.trim()) {
      newErrors.email = 'El correo electrónico es obligatorio';
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
      newErrors.email = 'Correo electrónico inválido';
    }

    if (!formData.phone?.trim()) {
      newErrors.phone = 'El teléfono es obligatorio';
    }

    if (!formData.address?.trim()) {
      newErrors.address = 'La dirección es obligatoria';
    }

    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('Éxito', 'El usuario ha sido creado');
      } else {
        await dispatch(updateUser({ ...formData, id: user!.id } as User));
        Alert.alert('Éxito', 'El usuario ha sido actualizado');
      }
      navigation.goBack();
    } catch (error) {
      Alert.alert('Error', 'Ha ocurrido un error');
    } 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}>Cancelar</Text>
        </TouchableOpacity>
        <Text style={styles.formTitle}>
          {mode === 'create' ? 'Crear usuario' : 'Editar usuario'}
        </Text>
        <TouchableOpacity onPress={handleSubmit} disabled={loading}>
          <Text style={[styles.saveButton, loading && styles.disabledButton]}>
            {loading ? 'Guardando...' : 'Guardar'}
          </Text>
        </TouchableOpacity>
      </View>

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

          <View style={styles.formGroup}>
            <Text style={styles.label}>Correo electrónico *</Text>
            <TextInput
              style={[styles.input, errors.email && styles.inputError]}
              value={formData.email}
              onChangeText={(value) => updateField('email', value)}
              placeholder="Ingresa el correo electrónico"
              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}>Teléfono *</Text>
            <TextInput
              style={[styles.input, errors.phone && styles.inputError]}
              value={formData.phone}
              onChangeText={(value) => updateField('phone', value)}
              placeholder="Ingresa el teléfono"
              keyboardType="phone-pad"
              editable={!loading}
            />
            {errors.phone && <Text style={styles.errorText}>{errors.phone}</Text>}
          </View>

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

// Stack de Navegación
const Stack = createStackNavigator();

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

// Estilos
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. Aplicación Flutter con 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('Lista de usuarios'),
        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('Cargando usuarios...'),
                ],
              ),
            );
          }

          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('Reintentar'),
                  ),
                ],
              ),
            );
          }

          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 se encontraron usuarios',
                    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('Editar'),
                        ],
                      ),
                    ),
                    PopupMenuItem(
                      value: 'delete',
                      child: Row(
                        children: [
                          Icon(Icons.delete, color: Colors.red),
                          SizedBox(width: 8),
                          Text('Eliminar', 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(
              'Creado: ${_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('Eliminar usuario'),
        content: Text('¿Deseas eliminar a ${user.name}?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('Cancelar'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              StoreProvider.of<AppState>(context).dispatch(DeleteUsersAction(user.id));
            },
            child: Text(
              'Eliminar',
              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 ? 'Crear usuario' : 'Editar usuario'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        actions: [
          if (!_isLoading)
            TextButton(
              onPressed: _saveUser,
              child: Text(
                'Guardar',
                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: 'Nombre',
                icon: Icons.person,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'El nombre es requerido';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              _buildTextField(
                controller: _emailController,
                label: 'Correo electrónico',
                icon: Icons.email,
                keyboardType: TextInputType.emailAddress,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'El correo electrónico es requerido';
                  }
                  if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
                    return 'Correo electrónico inválido';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              _buildTextField(
                controller: _phoneController,
                label: 'Teléfono',
                icon: Icons.phone,
                keyboardType: TextInputType.phone,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'El teléfono es requerido';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              _buildTextField(
                controller: _addressController,
                label: 'Dirección',
                icon: Icons.location_on,
                maxLines: 3,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'La dirección es requerida';
                  }
                  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 = 'Ha ocurrido un error: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
}

3. Progressive Web App (PWA)

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Aplicación PWA Móvil</title>
    
    <!-- PWA Meta Tags -->
    <meta name="theme-color" content="#2E86AB">
    <meta name="description" content="Progressive Web App para usuarios móviles">
    
    <!-- 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="PWA Móvil">
    <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">PWA Móvil</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">Usuarios</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">Activos</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">Pedidos</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">Ingresos</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>
                    Agregar usuario
                </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>
                    Filtrar
                </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>
                    Ordenar
                </button>
            </div>
        </section>

        <!-- Search Section -->
        <section class="mb-6">
            <div class="relative">
                <input 
                    type="text" 
                    id="searchInput"
                    placeholder="Buscar usuarios..."
                    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>Cargando usuarios...</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">Agregar usuario</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">Nombre *</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">Correo electrónico *</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">Teléfono *</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">Dirección *</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">Cancelar</button>
                    <button type="submit" id="saveBtn" class="btn btn-primary">
                        <span id="saveBtnText">Guardar</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 al cargar usuarios', '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 se encontraron usuarios</p>
                            <button class="btn btn-primary mt-4" onclick="app.showUserModal()">
                                Agregar primer usuario
                            </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('Usuario actualizado', '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('Usuario creado', '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 al guardar', '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 = 'El nombre es requerido';
                    document.getElementById('userName').classList.add('error');
                    isValid = false;
                }
                
                // Validate email
                if (!data.email.trim()) {
                    document.getElementById('emailError').textContent = 'El correo es requerido';
                    document.getElementById('userEmail').classList.add('error');
                    isValid = false;
                } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
                    document.getElementById('emailError').textContent = 'Correo electrónico inválido';
                    document.getElementById('userEmail').classList.add('error');
                    isValid = false;
                }
                
                // Validate phone
                if (!data.phone.trim()) {
                    document.getElementById('phoneError').textContent = 'El teléfono es requerido';
                    document.getElementById('userPhone').classList.add('error');
                    isValid = false;
                }
                
                // Validate address
                if (!data.address.trim()) {
                    document.getElementById('addressError').textContent = 'La dirección es requerida';
                    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 = 'Editar usuario';
                    this.showUserModal();
                }
            },
            
            async deleteUser(userId) {
                if (!confirm('¿Deseas eliminar este usuario?')) {
                    return;
                }
                
                try {
                    this.users = this.users.filter(u => u.id !== userId);
                    await this.saveUsersToCache(this.users);
                    this.renderUsers();
                    this.updateStats();
                    this.showToast('Usuario eliminado', 'success');
                    
                    if (this.isOnline) {
                        // Sync with server
                        this.syncData();
                    }
                } catch (error) {
                    console.error('Error deleting user:', error);
                    this.showToast('Error al
### 4. Service Worker para PWA
```javascript
// sw.js - Service Worker para 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');

Comparativa de enfoques de desarrollo móvil

CriterioiOS nativo (Swift)Android nativo (Kotlin)React NativeFlutterPWA
RendimientoÓptimoÓptimoBuenoMuy buenoBueno
UI/UXEstándar de plataformaEstándar de plataformaMultiplataformaMultiplataformaEstándar web
DesarrolloEspecífico de plataformaEspecífico de plataformaJavaScriptDartTecnologías web
Reutilización de códigoNingunaNinguna90%+95%+100%
Acceso a hardwareCompletoCompletoLimitadoLimitadoMuy limitado
App StoreNo
Funcionamiento offline
Notificaciones push
Proceso de actualizaciónApp StorePlay StoreOver-the-AirOver-the-AirAutomático

Comparativa de frameworks multiplataforma

React Native vs. Flutter

CaracterísticaReact NativeFlutter
LenguajeJavaScript/TypeScriptDart
Renderizado UIComponentes nativosMotor personalizado
RendimientoBuenoMuy bueno
Hot Reload
ComunidadGrandeEn crecimiento
Curva de aprendizajeMediaMedia
EcosistemaReactGoogle

Otras soluciones multiplataforma

  • Xamarin: C# con .NET (Microsoft)
  • Ionic: Tecnologías web con Capacitor
  • Cordova: Envoltorio de WebView
  • Qt: Framework de C++
  • Unity: C# para 3D/Juegos

Características y capacidades de PWA

Funcionalidades principales de PWA

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

// Aviso de instalación
window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    showInstallButton();
});

// Sincronización en segundo plano
if ('serviceWorker' in navigator && 'SyncManager' in window) {
    registration.sync.register('background-sync');
}

Limitaciones de PWA

  • iOS Safari: Soporte limitado
  • Acceso a hardware: Cámara, GPS con restricciones
  • Rendimiento: Más lento que aplicaciones nativas
  • Almacenamiento: Capacidad offline limitada
  • Notificaciones push: No disponible en todas las plataformas

Mejores prácticas de UI/UX móvil

Diseño responsivo

/* Diseño responsivo mobile-first */
.container {
    max-width: 100%;
    padding: 0 16px;
    margin: 0 auto;
}

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

/* Botones fáciles de tocar */
.btn {
    min-height: 44px;
    min-width: 44px;
    padding: 12px 16px;
}

Optimización de rendimiento

  • Carga perezosa: Imágenes y componentes
  • Separación de código: Imports dinámicos
  • Tamaño del bundle: Minimización y compresión
  • Optimización de imágenes: WebP, imágenes responsivas
  • Almacenamiento en caché: Service Worker y HTTP caching

Estrategias de testing móvil

Automatización de pruebas

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

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

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

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

Testing en dispositivos

  • Emuladores: iOS Simulator, Android Emulator
  • Dispositivos reales: Varios modelos y versiones de SO
  • Testing en la nube: BrowserStack, Sauce Labs
  • Beta testing: TestFlight, Google Play Beta

Mejores prácticas de seguridad móvil

Protección de datos

// Almacenamiento seguro
import { SecureStore } from 'expo-secure-store';

// Guardar datos sensibles de forma segura
await SecureStore.setItemAsync('userToken', token, {
    keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});

// Encriptación en tránsito
const https = require('https');
const crypto = require('crypto');

// Encriptar datos antes del almacenamiento
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')
    };
}

Autenticación

  • Biométrica: Touch ID, Face ID
  • OAuth 2.0: Flujos de autenticación seguros
  • JWT: Autenticación basada en tokens
  • 2FA: Autenticación de dos factores
  • Gestión de sesiones: Manejo seguro de sesiones

Optimización de rendimiento móvil

Métricas de rendimiento

// Monitoreo de rendimiento
import { performance } from 'perf_hooks';

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

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

Técnicas de optimización

  • Listas virtuales: Para grandes volúmenes de datos
  • Optimización de imágenes: Carga perezosa, WebP
  • Optimización de bundle: Tree shaking, separación de código
  • Gestión de memoria: Prevención de fugas
  • Optimización de red: Agrupación de requests, almacenamiento en caché

Ventajas e inconvenientes

Ventajas del desarrollo multiplataforma

  • Eficiencia de costos: Una base de código para todas las plataformas
  • Desarrollo más rápido: Lanzamiento al mercado más ágil
  • Consistencia: Experiencia de usuario uniforme
  • Mantenimiento: Actualizaciones más sencillas
  • Habilidades del equipo: Desarrolladores de JavaScript/Dart

Inconvenientes

  • Rendimiento: No siempre óptimo
  • Limitaciones de plataforma: Acceso limitado a APIs
  • Debugging: Análisis de errores más complejo
  • Retrasos en actualizaciones: Dependencia de actualizaciones del framework
  • Tamaño: Bundles de aplicación más grandes

Preguntas de examen frecuentes

  1. ¿Cuándo elegir nativo vs. multiplataforma? Nativo cuando hay requisitos altos de rendimiento. Multiplataforma cuando se busca lanzamiento rápido y presupuesto limitado.

  2. ¿Cuáles son las principales ventajas de las PWA? Sin dependencia de app store, actualizaciones automáticas, basadas en web, funcionamiento offline, económicas.

  3. Explica qué son los Service Workers. Service Workers son scripts de JavaScript que se ejecutan en segundo plano, permitiendo capacidades offline, almacenamiento en caché y notificaciones push.

  4. ¿Cómo se optimiza el rendimiento de una aplicación móvil? Carga perezosa, optimización de bundles, gestión de memoria, módulos nativos para rutas críticas, monitoreo de rendimiento.

Principales referencias

  1. https://reactnative.dev/
  2. https://flutter.dev/
  3. https://developer.apple.com/swift/
  4. https://developer.android.com/kotlin/
Volver al blog
Share:

Entradas relacionadas