La Motivación: ¿Por Qué Nació BLoC?
Imagina que estás construyendo una casa. La interfaz de usuario (UI) son las paredes, los muebles, los cuadros; todo lo que ves y con lo que interactúas. La lógica de negocio es la plomería, el sistema eléctrico, la calefacción; todo lo que hace que la casa funcione por detrás.
En una aplicación pequeña, podrías tener los cables y las tuberías a la vista, mezclados con los muebles. Al principio no es un gran problema. Pero, ¿qué pasa cuando la casa se vuelve una mansión? Si un enchufe deja de funcionar, tendrías que romper paredes y mover muebles solo para encontrar el cable defectuoso. Si quieres cambiar la calefacción, podrías dañar el sistema de agua sin querer. Es un caos.
Esto mismo sucedía en las primeras etapas de Flutter. Los desarrolladores a menudo mezclaban la lógica (qué hacer con los datos, cómo interactuar con una base de datos, etc.) directamente en los widgets de la UI. Esto provocaba varios problemas graves:
- Código Espagueti: La lógica estaba tan entrelazada con la UI que era imposible saber dónde empezaba una y terminaba la otra.
- Difícil de Testear: ¿Cómo pruebas la lógica de un pago si está dentro de un botón? Tendrías que simular un clic en el botón en lugar de simplemente probar la función de "pagar".
- Reutilización Imposible: Si querías usar la misma lógica de "iniciar sesión" en otra parte de la app, tenías que copiar y pegar un montón de código de UI junto con la lógica.
- Estado Inmanejable: A medida que la app crecía, era increíblemente difícil saber por qué la pantalla se actualizaba o de dónde venían los datos que se mostraban.
La solución era clara: necesitábamos un arquitecto. Un patrón que nos obligara a instalar la plomería y la electricidad (la lógica) en una sala de máquinas central, completamente separada de las habitaciones (la UI).
Aquí es donde nace BLoC. Su motivación principal es imponer una separación estricta y clara entre la presentación (UI) y la lógica de negocio.
La Filosofía y Lógica Central: El Flujo Reactivo
La filosofía de BLoC se basa en una idea muy simple pero poderosa: La UI notifica al BLoC sobre eventos del usuario, y el BLoC notifica a la UI sobre cambios de estado.
Pensemos en una analogía: una cafetería ☕.
- Tú (El Usuario): Llegas al mostrador.
- El Mostrador (La UI): Es donde interactúas. Ves los botones: "Pedir Espresso", "Pedir Latte".
-
Tu Pedido (El Evento): Cuando presionas "Pedir Latte", creas un Evento. El evento es una simple descripción de lo que quieres:
PedidoDeLatte
. No preparas el café, solo comunicas tu intención. -
El Barista (El BLoC): El barista toma tu pedido (el Evento
PedidoDeLatte
). Él es el único que sabe cómo preparar el café. Tiene la máquina, el conocimiento y los ingredientes. Esta es la lógica de negocio. No hablas directamente con la máquina de café; hablas con el barista. - El Proceso (La Lógica dentro del BLoC): El barista muele el grano, calienta la leche, extrae el espresso... realiza una serie de pasos.
-
La Pantalla de Pedidos (El Estado): Mientras esperas, miras una pantalla. Primero dice
PedidoRecibido
. Luego cambia aPreparandoCafe
. Finalmente, cambia aPedidoListo(tuLatte)
. Estos son los Estados. El barista va actualizando esta pantalla para que sepas qué está pasando. -
Recoges tu Café (La UI Reacciona al Estado): Cuando ves el estado
PedidoListo
, tu parte de la UI (tu vista) se actualiza: ahora tienes un café en la mano.
Esta es la esencia de BLoC:
-
Eventos (Events): Objetos inmutables que representan las intenciones del usuario o del sistema (ej:
BotonDeLoginPresionado
,DatosSolicitados
). - BLoC (Business Logic Component): La clase que recibe los Eventos, procesa la lógica correspondiente (hacer una llamada a una API, calcular algo) y emite nuevos Estados.
-
Estados (States): Objetos inmutables que representan una parte del estado de tu aplicación (ej:
Cargando
,Exitoso
,Error
). La UI simplemente escucha estos estados y se redibuja a sí misma según corresponda.
Este flujo es reactivo y predecible. La UI solo puede cambiar en respuesta a un nuevo estado emitido por el BLoC. Y el BLoC solo cambia su estado en respuesta a un evento. Sabes exactamente cómo fluye la información, lo que hace que depurar sea infinitamente más fácil.
¿Qué Soluciona y Qué Hace Exactamente?
BLoC soluciona el problema de la gestión del estado de una manera escalable y organizada.
¿Qué hace?
- Centraliza tu lógica de negocio: Todo el "cerebro" de una funcionalidad (autenticación, carrito de compras, perfil de usuario) vive en un solo lugar: su BLoC.
- Desacopla la UI de la lógica: Tu UI se vuelve "tonta". No sabe cómo iniciar sesión, solo sabe que hay un estado
Cargando
y debe mostrar un spinner, y un estadoAutenticado
y debe navegar a la pantalla principal. - Facilita las pruebas: Puedes probar tu lógica de inicio de sesión (el
AuthBloc
) de forma aislada. Le das un eventoLoginEvent
y verificas que emita los estadosLoadingState
y luegoAuthenticatedState
, todo sin renderizar un solo píxel en la pantalla. - Hace tu estado predecible: Al restringir el flujo a
Evento -> BLoC -> Estado
, siempre puedes rastrear por qué algo cambió. La libreríabloc
incluso viene con unBlocObserver
que te permite imprimir cada transición de estado en la consola.
¿Qué NO hace?
Esto es igualmente importante para entenderlo bien.
- No realiza la inyección de dependencias: BLoC no sabe cómo ser "provisto" a la UI. Para esto, se apoya en otra librería,
flutter_bloc
, que usaprovider
por debajo para inyectar eficientemente tus BLoCs en el árbol de widgets. - No es un framework de navegación: BLoC puede emitir un estado que desencadene una navegación (usando un
BlocListener
), pero la lógica de navegación en sí misma (usandoNavigator.push
oGoRouter
) está fuera de su responsabilidad. - No hace las llamadas a la API directamente: Tu BLoC orquestará la llamada, pero la implementación real de la llamada (el código que usa
http.get
odio.get
) debería estar en una capa separada, comúnmente llamada Repositorio o Proveedor de Datos. El BLoC llama al repositorio, y el repositorio obtiene los datos.
¿Cuándo Usarlo y Cuándo NO? (El Dilema del BLoC vs. Cubit)
No necesitas un BLoC para todo. Usar BLoC para manejar el estado de un simple checkbox es como usar un tráiler para llevar una bolsa del supermercado.
Cubit: El Hermano Menor y Simplificado
El equipo de BLoC creó Cubit, una versión más ligera y simple. La diferencia clave es:
-
BLoC: Usa Eventos (
add(LoginEvent())
). Es más verboso pero más trazable. Sabes exactamente qué causó el cambio de estado. -
Cubit: Expone funciones públicas (
login()
). Es más directo y requiere menos código.
Analogía:
- BLoC (El Restaurante Formal): Das una orden formal (Evento) al mesero, y no sabes exactamente qué pasa en la cocina, solo esperas el resultado (Estado). Es bueno para procesos complejos y para tener un registro claro de cada orden.
-
Cubit (El Puesto de Tacos): Le dices directamente al taquero: "¡Dame dos al pastor!" (llamas a una función
dameTacos(2)
). Es rápido, directo y eficiente para tareas simples.
Reglas de Oro:
-
Usa Cubit cuando:
- La lógica es simple. Por ejemplo, manejar el tema de la app (oscuro/claro), controlar el índice de un
BottomNavigationBar
, o manejar un formulario simple. - Los cambios de estado son el resultado de llamadas a funciones directas, no de una cadena compleja de eventos.
- Quieres escribir menos código para una funcionalidad sencilla.
- La lógica es simple. Por ejemplo, manejar el tema de la app (oscuro/claro), controlar el índice de un
-
Usa BLoC cuando:
- La lógica de negocio es compleja y tiene múltiples pasos. Un proceso de pago, por ejemplo:
ValidarFormulario -> ProcesarPagoConAPI -> CrearOrdenEnBD -> EnviarEmailDeConfirmacion
. - Necesitas trazabilidad. Quieres saber la secuencia exacta de eventos que llevaron a un estado de error. Esto es invaluable para la depuración.
- Los eventos pueden venir de múltiples fuentes: clics del usuario, respuestas de un WebSocket, notificaciones push, etc.
- Estás trabajando en un equipo grande y quieres que la lógica sea explícita y fácil de entender para todos.
- La lógica de negocio es compleja y tiene múltiples pasos. Un proceso de pago, por ejemplo:
Conclusión: Empieza con un Cubit. Si tu lógica empieza a volverse compleja y te encuentras poniendo demasiada inteligencia dentro de una sola función, es una señal de que necesitas refactorizar a un BLoC para manejar esa complejidad con eventos más granulares.
Rendimiento: ¿Cómo Evitar Reconstrucciones Innecesarias?
Una preocupación común con cualquier gestor de estado es el rendimiento. ¿Se reconstruirá toda mi pantalla cada vez que cambie un pequeño detalle del estado? Con BLoC, la respuesta es no, si lo usas correctamente.
La librería flutter_bloc
te da herramientas precisas para controlar qué se reconstruye y cuándo:
BlocBuilder
: Es el widget más común. Se reconstruye cada vez que el BLoC emite un nuevo estado. Por defecto, es bastante inteligente y no se reconstruirá si el estado emitido es idéntico al anterior.buildWhen
: Este es el superpoder deBlocBuilder
. Es una condición opcional que te permite especificar exactamente cuándo debe reconstruirse el widget.
* **Ejemplo:** En una pantalla de login, podrías tener un estado `LoginState` con propiedades `(email, password, status, errorMessage)`. Si el usuario solo está escribiendo en el campo de texto del email, no necesitas reconstruir el botón de login. Con `buildWhen`, puedes decirle que solo se reconstruya si el `status` (ej: `Loading`, `Success`, `Failure`) cambia.
<!-- end list -->
```dart
BlocBuilder<LoginBloc, LoginState>(
buildWhen: (previous, current) {
// Solo reconstruye el widget si el estado de la petición cambia,
// no si solo cambia el texto del email o la contraseña.
return previous.status != current.status;
},
builder: (context, state) {
// UI que muestra un spinner o un mensaje de error
if (state.status == LoginStatus.loading) {
return CircularProgressIndicator();
}
return ElevatedButton(onPressed: ..., child: Text('Login'));
},
)
```
-
BlocSelector
: Es aún más granular. Te permite escuchar cambios en una pequeña parte del estado.
* **Ejemplo:** Imagina un `UserState` con `(name, age, profileImageUrl)`. Si solo quieres reconstruir el widget del nombre cuando el nombre cambie, `BlocSelector` es perfecto. Evita reconstruir el widget de la imagen de perfil innecesariamente.
-
BlocListener
: Este widget es para efectos secundarios. No reconstruye ninguna UI. Su propósito es ejecutar una acción una sola vez en respuesta a un cambio de estado.
* **Usos perfectos:** Mostrar un `SnackBar` ("Usuario guardado con éxito"), un diálogo de error, o para la **navegación** (`Navigator.push(...)` cuando el estado sea `LoginSuccess`).
-
Equatable
: Para quebuildWhen
y el comportamiento por defecto deBlocBuilder
funcionen correctamente, tus objetos de estado deben poder compararse. La libreríaequatable
te ayuda a hacer esto fácilmente, simplemente listando las propiedades que definen la identidad del objeto.
La Implementación Ideal: Paso a Paso
Vamos a construir un ejemplo práctico: una pantalla que carga una lista de usuarios desde una API.
1. Estructura de Carpetas
Una buena organización es clave. Para una funcionalidad de "usuarios", tendrías algo así:
lib/
└── features/
└── users/
├── bloc/
│ ├── user_event.dart
│ ├── user_state.dart
│ └── user_bloc.dart
├── data/
│ ├── user_repository.dart
│ └── user_model.dart
└── presentation/
└── user_screen.dart
2. Definir los Estados (State)
El estado representa todo lo que la UI necesita saber. ¿Qué estados puede tener nuestra pantalla de usuarios?
-
UserInitial
: El estado inicial, antes de que hagamos nada. -
UserLoading
: Estamos cargando los usuarios. La UI debería mostrar un spinner. -
UserSuccess
: Tuvimos éxito. El estado contendrá la lista de usuarios. -
UserError
: Algo salió mal. El estado contendrá un mensaje de error.
user_state.dart
:
part of 'user_bloc.dart';
// Clase base abstracta para todos los estados
abstract class UserState extends Equatable {
const UserState();
@override
List<Object> get props => [];
}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserSuccess extends UserState {
final List<User> users; // Los datos que la UI necesita
const UserSuccess(this.users);
@override
List<Object> get props => [users];
}
class UserError extends UserState {
final String message; // El mensaje de error para la UI
const UserError(this.message);
@override
List<Object> get props => [message];
}
3. Definir los Eventos (Event)
El evento es lo que la UI envía al BLoC para desencadenar una acción.
-
FetchUsers
: Un evento para solicitar la carga de los usuarios.
user_event.dart
:
part of 'user_bloc.dart';
abstract class UserEvent extends Equatable {
const UserEvent();
@override
List<Object> get props => [];
}
// Único evento que necesitamos por ahora
class FetchUsers extends UserEvent {}
4. Crear el BLoC
Aquí es donde vive la magia. El BLoC escucha eventos y emite estados.
user_bloc.dart
:
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
// Importamos nuestro repositorio y modelo
import '../data/user_model.dart';
import '../data/user_repository.dart';
part 'user_event.dart';
part 'user_state.dart';
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository userRepository;
UserBloc({required this.userRepository}) : super(UserInitial()) {
// Aquí registramos el manejador para nuestro evento
on<FetchUsers>(_onFetchUsers);
}
// El manejador de eventos es una función asíncrona
Future<void> _onFetchUsers(FetchUsers event, Emitter<UserState> emit) async {
// 1. Emitimos el estado de carga inmediatamente
emit(UserLoading());
try {
// 2. Llamamos al repositorio para obtener los datos
final users = await userRepository.getUsers();
// 3. Si todo va bien, emitimos el estado de éxito con los datos
emit(UserSuccess(users));
} catch (e) {
// 4. Si hay un error, emitimos el estado de error
emit(UserError('Failed to fetch users: ${e.toString()}'));
}
}
}
Observa la separación de responsabilidades: el BLoC no sabe *cómo se obtienen los usuarios (HTTP, base de datos...), solo llama a userRepository.getUsers()
. El repositorio es el que se encarga de eso.*
5. Integrar con la UI
Finalmente, conectamos todo en la pantalla.
user_screen.dart
:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Importamos todo lo necesario
import '../bloc/user_bloc.dart';
import '../data/user_repository.dart';
class UsersScreen extends StatelessWidget {
const UsersScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Users')),
// 1. Proveemos el BLoC al árbol de widgets.
// BlocProvider crea la instancia del BLoC y se asegura de cerrarla (dispose) correctamente.
body: BlocProvider(
create: (context) => UserBloc(
// Normalmente inyectarías el repositorio con get_it o provider
userRepository: RepositoryProvider.of<UserRepository>(context),
)..add(FetchUsers()), // Añadimos el evento inicial para que cargue los datos al entrar.
child: const UserView(),
),
);
}
}
class UserView extends StatelessWidget {
const UserView({super.key});
@override
Widget build(BuildContext context) {
// 2. Usamos BlocBuilder para escuchar los cambios de estado y construir la UI.
return BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
// 3. Renderizamos diferentes widgets basados en el estado actual.
if (state is UserLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is UserSuccess) {
return ListView.builder(
itemCount: state.users.length,
itemBuilder: (context, index) {
final user = state.users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
}
if (state is UserError) {
return Center(child: Text(state.message));
}
// Estado inicial o por defecto
return const Center(child: Text('Press a button to fetch users.'));
},
);
}
}
¡Y eso es todo! Hemos creado una funcionalidad robusta, testeable y desacoplada. La UserView
es completamente "tonta", simplemente refleja el estado que le da el UserBloc
. Si mañana necesitas cambiar la fuente de datos de una API REST a Firebase, solo modificas el UserRepository
, y ni el BLoC ni la UI se enterarán del cambio. Ese es el verdadero poder de BLoC.
Top comments (0)