---
name: mobile-flutter
description: Flutter mejores prácticas. Cubre StatelessWidget/StatefulWidget/HookWidget, state management con Riverpod/BLoC/Provider, navegación con GoRouter y auto_route, platform channels, animaciones implícitas vs explícitas, performance con Impeller y const widgets, testing de widgets/integración/golden y anti-patrones críticos.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para React Native o Expo — Flutter usa Dart y tiene un modelo de renderizado completamente diferente; para React Native cargar `mobile-react-native`."
  - "No cargar para apps nativas iOS (Swift/SwiftUI) o Android (Kotlin/Compose) — Flutter abstrae las plataformas con su propio motor; para iOS cargar `swift-experto`, para Android cargar `kotlin-experto`."
  - "No cargar para errores de compilación Dart o configuración de Flutter SDK — para build errors usar las herramientas de diagnóstico de Flutter."
  - "No cargar para diseño de UI web (CSS, Tailwind) — Flutter usa su propio sistema de widgets sin HTML/CSS."
evolvable: true  # default para skill estandar
---
# Flutter Experto

## Cuándo NO cargar

- El proyecto es React Native o Expo — Flutter usa Dart con un motor de renderizado propio; para React Native cargar `mobile-react-native`.
- El proyecto es nativo iOS (Swift/SwiftUI) o Android (Kotlin/Compose) — cargar `swift-experto` o `kotlin-experto` según corresponda.
- Los errores son de compilación Dart, configuración del SDK o `pubspec.yaml` — usar las herramientas de diagnóstico de Flutter/Dart.
- El diseño UI es para web con HTML/CSS — Flutter no usa HTML; sus widgets son completamente distintos.

## Filosofía

Flutter compila a código nativo ARM con su propio motor de renderizado (Skia/Impeller).
No usa componentes nativos del OS — pinta cada pixel.

**Regla fundamental**: Todo es un widget. Un widget no es mutable por sí solo —
se reconstruye cuando su estado o el estado de su ancestro cambia.

---

## 1. Tipos de Widget — Cuándo Usar Cuál

| Tipo | Cuándo |
|------|--------|
| **StatelessWidget** | Sin estado mutable, solo recibe props |
| **StatefulWidget** | Estado local al widget (animaciones, controllers) |
| **HookWidget** | Alternativa menos verbosa a StatefulWidget |
| **ConsumerWidget** | Cuando se usa Riverpod |

```dart
// StatelessWidget — patrón base
class TarjetaUsuario extends StatelessWidget {
  const TarjetaUsuario({super.key, required this.usuario, this.onTap});
  final Usuario usuario;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        title: Text(usuario.nombre),
        subtitle: Text(usuario.email),
        trailing: const Icon(Icons.chevron_right),
        onTap: onTap,
      ),
    );
  }
}
```

---

## 2. State Management — Riverpod (Recomendado)

```dart
@riverpod
Future<List<Usuario>> listaUsuarios(ListaUsuariosRef ref) async {
  final repo = ref.watch(usuarioRepositoryProvider);
  return repo.obtenerTodos();
}

// Notifier con estado mutable
@riverpod
class CarritoNotifier extends _$CarritoNotifier {
  @override
  List<Producto> build() => [];

  void agregar(Producto p) { state = [...state, p]; }
  void eliminar(String id) { state = state.where((p) => p.id != id).toList(); }
}

// Uso con asyncValue.when()
asyncUsuarios.when(
  data: (usuarios) => ListView.builder(...),
  loading: () => const CircularProgressIndicator(),
  error: (e, st) => Text('Error: $e'),
);
```

---

## 3. Animaciones — Implícitas vs Explícitas

**Preferir implícitas** para transiciones simples:

```dart
AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeInOut,
  width: _expandido ? 200.0 : 100.0,
  height: _expandido ? 200.0 : 100.0,
)
```

Usar **explícitas** para animaciones complejas con `AnimationController`.
SIEMPRE disponer controllers en `dispose()`.

---

## 4. Performance — Reglas Críticas

- **`const` SIEMPRE que sea posible** — evita reconstrucción.
- **RepaintBoundary** para aislar subárboles que cambian frecuentemente.
- **`compute()`** para procesamiento pesado en isolate separado.
- **Impeller** habilitado por defecto en iOS y Android 3.19+.
- **Platform Views**: minimizar — tienen overhead significativo.

```dart
// BIEN: const evita reconstrucción
child: const Padding(
  padding: EdgeInsets.all(16),
  child: Text('Título fijo'),
)

// CPU-intensivo: usar isolate
final resultado = await compute(procesarJsonGrande, jsonString);
```

---

## 5. Anti-patrones Críticos

### BuildContext después de await

```dart
// MAL: context puede ser inválido
Future<void> _guardar() async {
  await servicio.guardar(datos);
  Navigator.of(context).pop(); // puede crashear

// BIEN: verificar mounted
  if (!mounted) return;
  Navigator.of(context).pop();
}
```

### setState en subárboles grandes

NUNCA `setState` en la raíz — reconstruye toda la pantalla.
Aislar el estado al widget que lo necesita, o usar Riverpod/BLoC.

### Dispose olvidado

```dart
// NUNCA olvidar:
@override
void dispose() {
  _controller.dispose();       // AnimationController
  _scrollController.dispose(); // ScrollController
  _textController.dispose();   // TextEditingController
  _focusNode.dispose();        // FocusNode
  super.dispose();
}
```

---

## 6. Testing

- **Widget tests**: `tester.pumpWidget()` + `find.text()` + `expect()`.
- **Golden tests**: `matchesGoldenFile()` para comparación visual.
- **Actualizar goldens**: `flutter test --update-goldens`.
- **Modo perfil**: `flutter run --profile` para detectar jank.

---

## Checklist Flutter

- [ ] Widgets const donde sea posible
- [ ] Controllers dispuestos en `dispose()`
- [ ] `mounted` verificado después de cada await
- [ ] State management con Riverpod o BLoC (no setState global)
- [ ] RepaintBoundary en animaciones frecuentes
- [ ] Compute para CPU-intensivo
- [ ] Widget tests para componentes clave
- [ ] Golden tests para componentes visuales críticos

Para implementaciones completas de BLoC, GoRouter, Platform Channels, animaciones explícitas y tests, ver [recursos/ejemplos-completos.md](recursos/ejemplos-completos.md).

## Gotchas / Errores comunes no obvios

**Usar `BuildContext` después de un `await` sin verificar `mounted` causa `setState() called after dispose()`**: si el widget se desmontó mientras la operación async estaba en progreso, `context` ya no es válido. Causa: Flutter no cancela automáticamente operaciones async cuando el widget se destruye. Fix: siempre verificar `if (!mounted) return;` inmediatamente después de cada `await` antes de usar `context`, `setState` o `Navigator`.

**`const` en widgets con `GlobalKey` o con providers que cambian no previene reconstrucción**: `const TarjetaProducto(key: Key('prod-1'))` puede parecer que se cachea, pero si el `Provider` padre cambia y el widget consume datos del provider, se reconstruirá de todas formas. Causa: `const` en Flutter evita reconstrucción solo cuando el constructor recibe los mismos argumentos — no protege contra cambios en providers heredados. Fix: para aislar reconstrucciones costosas usar `Consumer` con un `builder` granular que solo consume el slice de estado necesario.

**`Riverpod` con `ref.watch` dentro de un callback (como `onPressed`) lanza `ProviderException`**: `ref.watch` solo puede llamarse en el cuerpo del `build` o en `ConsumerWidget` — llamarlo en `onPressed`, `initState` o callbacks async lanza error en runtime. Causa: `ref.watch` registra una suscripción que necesita el contexto de build activo. Fix: usar `ref.read` dentro de callbacks y `ref.watch` solo en el cuerpo del `build`.

**`AnimationController` sin `vsync: this` en un `StatefulWidget` que no implementa `TickerProviderStateMixin` causa error silencioso de rendimiento**: si se pasa `vsync: this` pero el widget solo implementa `SingleTickerProviderStateMixin` y luego se agregan más controllers, el segundo controller falla. Causa: `SingleTickerProviderStateMixin` solo soporta un ticker — el segundo lanza `FlutterError` en debug. Fix: implementar `TickerProviderStateMixin` (plural) cuando el widget necesita múltiples `AnimationController`.
