MVVM es un patrón que separa vista, datos y lógica de presentación.
Cuando una aplicación crece, separar archivos no basta: también necesitas decidir qué responsabilidad pertenece a cada pieza.
En Flutter, el rey indiscutible (gracias a su similitud con la forma de trabajar de Provider) es el MVVM (Model - View - ViewModel).
No te asustes por las siglas. Es simplemente una regla para saber dónde escribir cada línea de código para no mezclar churras con merinas.
¿Qué es cada pieza?
Vamos a mapear los conceptos teóricos a lo que ya sabemos de Flutter:
- ¿Qué es? Son los datos puros. Las clases que representan “cosas” del mundo real.
- ¿Qué hace? Representa datos y, según el diseño, puede incluir reglas propias del dominio. No conoce la interfaz.
- En tu código: Son tus clases con
fromJsonytoJson. - Ejemplo:
class Producto { final String nombre; final double precio; ... }
- ¿Qué es? Es la Interfaz de Usuario. Lo que el usuario ve y toca.
- ¿Qué hace? Pinta la pantalla basándose en el estado actual y captura los clics del usuario.
- En tu código: Son tus
Widgets(Screens). - Ejemplo:
class PantallaCarrito extends StatelessWidget { ... }
-
¿Qué es? Es el intermediario. El cerebro.
-
¿Qué hace?
-
Guarda el Estado (la lista de productos, si está cargando, etc.).
-
Contiene la Lógica de Negocio (calcular el total, validar un cupón).
-
Expone métodos para que la Vista los llame.
-
En tu código: puede ser una clase
ChangeNotifierque exponemos mediante Provider.
Flujo de trabajo
En esta variante, la vista recibe del ViewModel el estado que necesita y le envía las acciones del usuario.
Usuario toca un botón en la Vista (onTap).
Vista llama a una función del ViewModel (provider.agregar()).
ViewModel procesa la lógica, actualiza sus Modelos internos y notifica (notifyListeners).
Vista escucha el cambio y se redibuja automáticamente.
Ejemplo práctico: un carrito de compra
Vamos a ver cómo se ve esto en código real.
Datos tontos. Solo estructura.
class Item {
final String nombre;
final double precio;
Item({required this.nombre, required this.precio});
}
Aquí vive la lógica. Si mañana decides que el carrito tiene un máximo de 10 items, cambias este archivo. La vista ni se entera.
import 'package:flutter/material.dart';
import '../models/item.dart';
class CartProvider extends ChangeNotifier { // Esto es el ViewModel
// Estado privado
final List<Item> _items = [];
// Getters para la vista (Estado público)
List<Item> get items => List.unmodifiable(_items);
double get precioTotal {
return _items.fold(0, (total, item) => total + item.precio);
}
// Lógica de Negocio (Acciones)
void agregarItem(Item item) {
_items.add(item);
notifyListeners(); // ¡Avisa a la Vista!
}
void limpiarCarrito() {
_items.clear();
notifyListeners();
}
}
La vista es “tonta”. No calcula sumas. Solo muestra lo que le dice el ViewModel.
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Escuchamos al ViewModel (Watch)
final cart = context.watch<CartProvider>();
return Scaffold(
appBar: AppBar(title: Text("Mi Carrito")),
body: Column(
children: [
// Lista de items
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (ctx, i) => ListTile(
title: Text(cart.items[i].nombre),
trailing: Text("\$${cart.items[i].precio}"),
),
),
),
// Total y Botón
Container(
padding: EdgeInsets.all(20),
color: Colors.green[100],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Total: \$${cart.precioTotal}", // El cálculo lo hizo el ViewModel
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
ElevatedButton(
onPressed: () {
// Acción del usuario -> Llamada al ViewModel
context.read<CartProvider>().limpiarCarrito();
},
child: Text("Pagar"),
)
],
),
)
],
),
);
}
}
Qué aporta esta separación
Podrías pensar: “¿Para qué crear tres archivos si puedo hacerlo todo en el Widget?”.
- Testabilidad: Puedes probar tu
CartProvider(verificar que la suma funciona) sin tener que arrancar un emulador ni dibujar píxeles. - Mantenibilidad: Si el diseñador cambia la pantalla de roja a azul, solo tocas la Vista. Si el negocio cambia la regla de impuestos, solo tocas el ViewModel.
- Reutilización: Puedes usar el mismo
CartProviderpara mostrar el número de items en un icono pequeño en elHomeScreen.