ProgramaciónEscrito por persona

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.

Juan Escobar

6 min de lectura

flutterdartmvvmmobiletesting

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:

  1. CompositionRoot (lib/core/composition_root.dart): fábrica central de dependencias. Se consume vía CompositionRoot.of(context) desde cualquier pantalla.
  2. ViewModel (ChangeNotifier): encapsula la lógica de presentación y no depende del árbol de widgets.
  3. StateContainer + State: StateContainer gestiona el ciclo de vida del ViewModel y ejecuta setState(); 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. late para 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) vs StatefulWidget (con estado local).
  • Ciclo de vida: initState() → didChangeDependencies() (obtener dependencias) → build() (UI) → dispose() (limpieza de controladores).
  • Manejo de context: evita usar context después de un await sin verificar if (!mounted) return;.
  • Rendimiento: usa ListView.builder para listas largas en lugar de Column.

Estrategia offline-first y seguridad

  • Base de datos: SQLite gestionado con sqflite (lib/data/database_helper.dart).
  • Migraciones: alterar el esquema exige incrementar version e implementar onUpgrade. 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 en SharedPreferences (texto plano).
  • Control de sesión: un SessionGuardService valida 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:

  1. Valida que el test pueda fallar alterando la condición esperada.
  2. Compara contra datos de fixture, no contra propiedades del mismo objeto bajo prueba.
  3. 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 en didChangeDependencies() con CompositionRoot.of(context) → registrar ruta en main.dart → agregar tests.
  • Nuevo campo en entidad: actualizar Entity en domain/ → Model (fromMap/toMap) en data/ → database_helper.dart (subir versión de DB + onUpgrade) → View → fixtures.
  • Agregar paquete: ejecutar flutter pub add <paquete> y reiniciar el proceso con flutter run.

Trampas comunes

  1. Usar context tras un await sin comprobar mounted.
  2. Omitir dispose() en controladores: fugas de memoria.
  3. Ejecutar lógica pesada dentro de build().
  4. Confiar en Hot Reload tras cambiar pubspec.yaml, main() o archivos estáticos (hace falta Hot Restart o relanzar).
  5. Abusar del operador !: termina en NullThrownError.

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.