Iniciación a Flutter
Una guía rápida para crear un proyecto Flutter, organizar la arquitectura MVVM offline-first y evitar las trampas más comunes al arrancar.
Flutter te deja construir apps para móvil, web y escritorio con un solo código. Esta guía no es un tour de widgets: es el kit mínimo para crear el proyecto bien, declarar assets, armar una arquitectura offline-first y no caerte en las trampas de siempre.
Comandos
Para crear el proyecto de forma simple normalmente se usa:
flutter create mi_app
Sin embargo se recomienda crearlo con --org para fijar el identificador del paquete desde el inicio y no reorganizarlo a mano después:
flutter create --org com.example mi_app
Otros comandos clave:
flutter doctor # Validar si la instalación y el entorno están correctos
flutter pub get # Instalar dependencias (similar a pnpm/npm install)
flutter run # Ejecutar el proyecto (o lanzarlo desde VS Code)
flutter clean # Limpiar dependencias y build (equivalente a borrar node_modules)
pod install # En ios/ cuando hay problemas de incompatibilidad (evitar si es posible)
Configuración de assets y .env en pubspec.yaml
El pubspec.yaml es el manifiesto del proyecto (equivalente a package.json en Node). Define dependencias, versiones y declara los recursos estáticos empaquetados en el build.
Regla de oro de YAML: la indentación es estrictamente de 2 espacios. Nunca uses pestañas.
name: mi_proyecto
description: "Ejemplo de configuración de assets y env"
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
flutter_dotenv: ^5.1.0 # Librería requerida para leer archivos .env
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
# Declaración de ASSETS y archivo .ENV (rutas desde la raíz)
assets:
- .env
- assets/images/
- assets/icons/logo.png
Estructura requerida en la raíz del proyecto:
mi_proyecto/
├── .env ← Archivo de variables de entorno en la raíz
├── assets/
│ ├── icons/
│ │ └── logo.png
│ └── images/
│ ├── banner.png
│ └── avatar.png
├── lib/
│ └── main.dart
└── pubspec.yaml ← Archivo de configuración
Carga del .env en lib/main.dart:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env");
runApp(const MyApp());
}
// Para usar una variable en cualquier parte:
// String apiKey = dotenv.env['API_KEY'] ?? 'default_value';
Arquitectura: MVVM (offline-first + Composition Root)
Estructura recomendada con separación limpia de responsabilidades y composición de dependencias a mano:
lib/
├── main.dart ← entry point: .env, rutas, arranque
├── core/ ← transversal: composition_root, theme, conectividad, utils
├── domain/ ← reglas puras (Dart puro, sin Flutter/HTTP/SQL)
│ ├── entities/ ← objetos de negocio
│ └── repositories/ ← interfaces (contratos)
├── data/ ← implementación real
│ ├── models/ ← entidad + fromMap()/toMap()
│ ├── datasources/ ← remote/ (API) y local/ (SQLite)
│ ├── repositories/ ← implementan domain/, deciden red vs local
│ └── database_helper.dart
└── presentation/ ← UI
├── views/ ← pantallas
├── components/ ← widgets reutilizables
├── viewmodels/ ← lógica de cada pantalla (lo que se testea)
└── state/ ← puente viewmodel → árbol de widgets
test/ ← espejo de lib/, con helpers/fakes.dart y helpers/fixtures.dart
- Lógica de negocio:
viewmodels/+repositories/ - Apariencia y UI:
views/+components/ - Origen de datos:
datasources/
Flujo de datos (ejemplo: sincronización)
Vista → StateContainer crea el ViewModel → ViewModel pide datos al Repository
→ Repository: si hay internet, consulta la API y guarda en SQLite;
si no, consulta SQLite. Los datos siempre se leen desde SQLite.
La red alimenta la base. La UI no habla con la API: habla con el repositorio, y el repositorio siempre entrega lo que hay en local.
Manejo de estado: patrón propio
Sin librerías externas como Provider, Riverpod, Bloc o GetX:
CompositionRoot(lib/core/composition_root.dart): fábrica central de dependencias. Se consume víaCompositionRoot.of(context)desde cualquier pantalla.ViewModel(ChangeNotifier): encapsula la lógica de presentación y no depende del árbol de widgets.StateContainer+State:StateContainergestiona el ciclo de vida del ViewModel y ejecutasetState();State(InheritedWidget) expone el estado al subárbol de componentes.
Dart y Flutter: conceptos esenciales
- Variables:
final(runtime) por defecto.const(compilación) para optimizar re-renderizados.latepara inicialización pospuesta (DI/tests). - Null safety:
?(nullable),!(fuerza no-null — usar con cautela),??(fallback),?.(acceso seguro). - Asincronía:
async/await/Future, equivalentes a Promises. - Records:
(A, B)para retornos múltiples sin crear DTOs temporales. - Privacidad: el prefijo
_limita la visibilidad al archivo actual. - Widgets:
StatelessWidget(estático) vsStatefulWidget(con estado local). - Ciclo de vida:
initState()→didChangeDependencies()(obtener dependencias) →build()(UI) →dispose()(limpieza de controladores). - Manejo de context: evita usar
contextdespués de unawaitsin verificarif (!mounted) return;. - Rendimiento: usa
ListView.builderpara listas largas en lugar deColumn.
Estrategia offline-first y seguridad
- Base de datos: SQLite gestionado con
sqflite(lib/data/database_helper.dart). - Migraciones: alterar el esquema exige incrementar
versione implementaronUpgrade. Si no, en instalaciones existentes aparece el clásico"no such column". - Autenticación: Auth0 mediante un
AuthService. - Almacenamiento seguro: tokens y secretos en
flutter_secure_storage(Keychain/Keystore). Nunca guardes credenciales enSharedPreferences(texto plano). - Control de sesión: un
SessionGuardServicevalida expiraciones con reloj de servidor o con el contador del sistema (SystemClock.elapsedRealtime()).
Testing
flutter test # Ejecutar toda la suite
flutter test test/data/services/ # Ejecutar una carpeta específica
flutter test --name "texto" # Filtrar por nombre de test
Organiza con setUp(), group(), when(...).thenAnswer(...) y expect(...).
Aprovecha test/helpers/fakes.dart para mocks y test/helpers/fixtures.dart para datos estáticos de prueba.
Principios:
- Valida que el test pueda fallar alterando la condición esperada.
- Compara contra datos de fixture, no contra propiedades del mismo objeto bajo prueba.
- Haz pruebas de mutación: rompe deliberadamente la lógica y comprueba si el test detecta la falla.
Recetas rápidas y trampas comunes
Recetas
- Pantalla nueva: crear vista en
views/→ ViewModel (ChangeNotifier) → resolver dependencias endidChangeDependencies()conCompositionRoot.of(context)→ registrar ruta enmain.dart→ agregar tests. - Nuevo campo en entidad: actualizar Entity en
domain/→ Model (fromMap/toMap) endata/→database_helper.dart(subir versión de DB +onUpgrade) → View → fixtures. - Agregar paquete: ejecutar
flutter pub add <paquete>y reiniciar el proceso conflutter run.
Trampas comunes
- Usar
contexttras unawaitsin comprobarmounted. - Omitir
dispose()en controladores: fugas de memoria. - Ejecutar lógica pesada dentro de
build(). - Confiar en Hot Reload tras cambiar
pubspec.yaml,main()o archivos estáticos (hace falta Hot Restart o relanzar). - Abusar del operador
!: termina enNullThrownError.
Resumen
Crea el proyecto con --org, declara assets en el pubspec.yaml y mantén la UI lejos de la red: el repositorio decide, SQLite es la fuente que lee la pantalla. Con Composition Root, ViewModels testeables y las cinco trampas de arriba en la cabeza, el resto de Flutter se aprende sobre una base que no se te desarma a la semana.
Comunidad abierta
¿Listo para prender el cerebro?
Explora artículos claros, gratuitos y hechos para compartirse. Aprender es gratis. Compartir también.