Flutter BLoC vs Riverpod en 2025: Un Marco de Decisión para Producción
Tras lanzar varias aplicaciones Flutter en producción con BLoC y Riverpod, aquí presentamos un marco práctico para elegir entre ellos — no basado en ejemplos de juguete, sino en compromisos reales que hemos encontrado en el trabajo.
La Respuesta Corta que Nadie Quiere
Ninguno es universalmente mejor. La respuesta depende de tu equipo, la complejidad del proyecto y cuán en serio te tomas la testabilidad. Si quieres una línea: usa BLoC para lógica de negocio compleja con varios ingenieros, Riverpod para apps más pequeñas o equipos que prefieren menos ceremonia.
El resto de este artículo explica por qué.
Qué es Realmente BLoC
BLoC significa Business Logic Component. Es un patrón de gestión de estado construido alrededor de streams y la separación de la lógica de negocio de la UI. El paquete flutter_bloc proporciona la implementación.
El modelo mental central: tu UI despacha Events, tu BLoC los procesa y emite States, y tus widgets reaccionan a los cambios de estado.
// Event
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested({required this.email, required this.password});
}
// State
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
AuthAuthenticated(this.user);
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
// BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _repository;
AuthBloc(this._repository) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final user = await _repository.login(event.email, event.password);
emit(AuthAuthenticated(user));
} catch (e) {
emit(AuthError(e.toString()));
}
}
}Ventajas de BLoC:
- Arquitectura event-driven explícita. Cada transición de estado tiene una causa con nombre.
- Excelente testabilidad. Puedes hacer unit tests del BLoC sin tocar la UI.
- Separación clara de responsabilidades. La UI no sabe nada sobre cómo cambia el estado.
- El paquete
flutter_bloc_testhace que probar streams sea sencillo.
Desventajas de BLoC:
- Boilerplate significativo, especialmente para estado simple.
- Las clases Event aumentan el número de archivos y la carga cognitiva en features pequeñas.
- Cubit existe para reducir el boilerplate (métodos en lugar de events), pero los equipos suelen mezclar BLoC y Cubit de forma inconsistente.
Qué es Realmente Riverpod
Riverpod es una reimaginación completa de Provider. Es compile-safe, soporta generación de código (riverpod_generator), y usa el concepto de Providers — contenedores de estado reactivos tipados.
// Un simple async provider que obtiene un perfil de usuario
@riverpod
Future<UserProfile> userProfile(UserProfileRef ref, String userId) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchProfile(userId);
}
// En un widget
class ProfilePage extends ConsumerWidget {
final String userId;
const ProfilePage({required this.userId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final profile = ref.watch(userProfileProvider(userId));
return profile.when(
data: (user) => ProfileContent(user: user),
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorView(message: e.toString()),
);
}
}Ventajas de Riverpod:
- Menos boilerplate para patrones comunes (obtener datos, transformarlos, mostrarlos).
- La generación de código con la anotación
@riverpodmaneja las partes tediosas. - Los providers son componibles — un provider puede depender de otro.
- Mejor soporte para estado computado/derivado sin mappers explícitos.
ref.invalidate()yref.refresh()ofrecen control explícito sobre la invalidación de caché.
Desventajas de Riverpod:
- El modelo mental tarda en interiorizarse, especialmente
ref.watchvsref.readvsref.listen. - La mutación de estado para flujos complejos puede volverse enredada.
- Menos explícito sobre "qué causó este cambio de estado" en comparación con los eventos nombrados de BLoC.
- Testear
ConsumerWidgetrequiere configurar unProviderScope.
Escenarios Reales de Producción
Escenario 1: Formulario de múltiples pasos con validación y envío
Aquí BLoC destaca. Tienes eventos distintos (FieldChanged, StepAdvanced, FormSubmitted, RetryRequested) y estados distintos (FormEditing, FormValidating, FormSubmitting, FormSuccess, FormError).
Ganador: BLoC
Escenario 2: Obtención de datos con caché en varias pantallas
Un perfil de usuario que se obtiene una vez y se comparte entre múltiples pantallas. Con Riverpod, defines un provider y cualquier pantalla que lo necesite llama a ref.watch. El caché se gestiona automáticamente. La invalidación al cerrar sesión es ref.invalidate(userProfileProvider).
Ganador: Riverpod
Escenario 3: Lógica de dominio compleja con cubits colaboradores
Una pantalla de gestión de pedidos donde un CartCubit, PaymentCubit y OrderCubit necesitan coordinarse. Ambos enfoques funcionan, pero el de BLoC es más explícito sobre la cadena de causalidad.
Ganador: Empate — BLoC para auditabilidad, Riverpod para menos boilerplate
El Factor del Equipo
Esta es la variable más subestimada.
Si tu equipo tiene 3+ desarrolladores Flutter y necesitas límites claros de ownership — la verbosidad de BLoC es una ventaja. Cada archivo tiene nombre, cada evento es explícito. Los desarrolladores nuevos pueden leer el BLoC y entender qué hace la feature sin ejecutarlo.
Si tienes un desarrollador en solitario o un equipo pequeño de 2 personas y te mueves rápido, Riverpod elimina suficiente ceremonia para acelerar significativamente el desarrollo.
Nuestro Default Actual en Codevia
Usamos BLoC (variante Cubit) para features complejas con mucha lógica de negocio y Riverpod para obtención de datos simples y estado computado.
Aplicamos una regla estricta: un BlocConsumer o BlocListener por cubit por página. Esto previene el antipatrón donde el mismo cubit tiene listeners dispersos en widgets anidados.
El Marco de Decisión
Hazte estas preguntas en orden:
- ¿Tiene la feature lógica de negocio compleja con múltiples fuentes de eventos? → BLoC
- ¿El estado representa principalmente el resultado de una operación async? → Riverpod
- ¿El equipo es grande (4+ personas) y necesita ownership claro? → BLoC
- ¿Construyes una herramienta interna pequeña o avanzas rápido en un MVP? → Riverpod
- ¿Ya tienes uno en el codebase? → Úsalo de forma consistente, no mezcles
El peor resultado es un codebase que usa ambos de forma inconsistente. Elige uno por proyecto y aplícalo.
Testing de Ambos
Ambos son testables. BLoC tiene el paquete bloc_test con un DSL limpio:
blocTest<AuthBloc, AuthState>(
'emits [AuthLoading, AuthAuthenticated] on successful login',
build: () => AuthBloc(mockRepository),
act: (bloc) => bloc.add(LoginRequested(email: '[email protected]', password: '123')),
expect: () => [AuthLoading(), isA<AuthAuthenticated>()],
);Los tests de Riverpod requieren un ProviderContainer:
test('userProfile fetches and returns a profile', () async {
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(mockRepository),
],
);
final profile = await container.read(userProfileProvider('user-1').future);
expect(profile.id, equals('user-1'));
});Ambos son limpios. Los tests de BLoC son ligeramente más legibles para testing de comportamiento. Los tests de Riverpod son más simples para escenarios de obtención de datos.
Conclusión
No elijas basándote en estrellas de GitHub o charlas de conferencias. Elige basándote en:
- Tamaño y experiencia del equipo
- Complejidad de las features y densidad de eventos
- Cuánto valoras la causalidad explícita vs el código conciso
Si tienes dudas, empieza con Riverpod en proyectos nuevos. Si llegas a un punto donde las transiciones de estado son difíciles de razonar, introduce BLoC para esas features específicas. Puedes mezclarlos a nivel de feature — pero no a nivel de widget.