tutoriales.com

Gestión de Estado Reactiva con BLoC en Flutter: Un Tutorial Completo

Este tutorial te guiará a través de la implementación del patrón BLoC (Business Logic Component) para una gestión de estado reactiva en Flutter. Aprenderás desde los fundamentos hasta la creación de una aplicación de ejemplo, manejando eventos, estados y datos de manera eficiente y escalable.

Intermedio25 min de lectura4 views16 de marzo de 2026Reportar error

¡Bienvenido a este tutorial completo sobre la gestión de estado con BLoC en Flutter! 🎉 Si has trabajado con Flutter, sabes que la gestión de estado es un pilar fundamental para construir aplicaciones robustas y mantenibles. BLoC, o Business Logic Component, es uno de los patrones más populares y poderosos para lograrlo, ofreciendo una clara separación de preocupaciones y una arquitectura reactiva.

¿Qué es BLoC y por qué usarlo? 🧐

BLoC es un patrón de arquitectura que ayuda a separar la lógica de negocio de la interfaz de usuario. Su principio fundamental es que todo en la aplicación debe ser un Stream de eventos o un Stream de estados. Esto significa que la lógica de negocio procesa eventos de entrada y emite nuevos estados como salida.

Ventajas de BLoC:

  • Separación de preocupaciones: La lógica de negocio está completamente desacoplada de la UI, haciendo el código más modular y fácil de entender.
  • Reusabilidad: Los BLoCs pueden ser reutilizados en diferentes partes de la aplicación o incluso en diferentes proyectos.
  • Testabilidad: Al tener una lógica de negocio aislada, es mucho más sencillo escribir pruebas unitarias para cada BLoC.
  • Predecibilidad: Dada una serie de eventos, siempre obtendremos el mismo estado de salida, lo que facilita la depuración y el mantenimiento.
  • Escalabilidad: Ideal para proyectos grandes donde el estado puede volverse complejo rápidamente.
💡 Consejo: Piensa en BLoC como un "transformador" de eventos a estados. La UI envía eventos, el BLoC los procesa y la UI reacciona a los nuevos estados.

Conceptos Clave de BLoC 🔑

Antes de sumergirnos en el código, es crucial entender los componentes básicos del ecosistema BLoC:

  1. Eventos (Events): Representan acciones o entradas del usuario (o de otras partes de la aplicación). Son la entrada al BLoC.
  2. Estados (States): Representan el estado actual de la aplicación o de una parte de ella. Son la salida del BLoC.
  3. BLoC: La clase central que toma Streams de eventos, los procesa utilizando lógica de negocio y emite Streams de estados.
  4. bloc_builder: Un widget que reacciona a los cambios de estado emitidos por un BLoC y reconstruye una parte de la interfaz de usuario.
  5. bloc_listener: Un widget que escucha los cambios de estado de un BLoC y realiza acciones una sola vez (navegación, mostrar un snackbar, etc.) sin reconstruir la UI.
  6. bloc_provider: Un widget que hace un BLoC disponible para sus widgets descendientes en el árbol de widgets.
UI Reacciona al Estado Añadir Evento BLoC (Procesa Evento) Emitir Estado FLUJO BLoC

Preparación del Entorno 🛠️

Para seguir este tutorial, necesitarás tener Flutter instalado y configurado. Además, utilizaremos el paquete flutter_bloc, que es la implementación oficial y más popular del patrón BLoC en Flutter.

1. Crear un Nuevo Proyecto Flutter

Si aún no tienes un proyecto, crea uno con el siguiente comando:

flutter create bloc_counter_app
cd bloc_counter_app

2. Añadir Dependencias

Abre el archivo pubspec.yaml y añade las siguientes dependencias bajo dependencies:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.3
  equatable: ^2.0.5 # Utilizado para comparar objetos de forma sencilla (eventos y estados)

Guarda el archivo y ejecuta flutter pub get en tu terminal para obtener los paquetes.


Ejemplo Práctico: Un Contador Simple con BLoC 🔢

Comenzaremos con un ejemplo clásico: una aplicación de contador. Esta aplicación tendrá dos botones (+ y -) para incrementar y decrementar un número, y un texto que mostrará el valor actual.

1. Definir Eventos (Events) 📝

Los eventos representan las intenciones del usuario. En nuestro contador, tendremos dos eventos: incrementar y decrementar.

Crea una carpeta lib/counter y dentro un archivo counter_event.dart:

part of 'counter_bloc.dart';

@immutable
abstract class CounterEvent extends Equatable {
  const CounterEvent();

  @override
  List<Object> get props => [];
}

class IncrementCounter extends CounterEvent {
  const IncrementCounter();

  @override
  String toString() => 'IncrementCounter';
}

class DecrementCounter extends CounterEvent {
  const DecrementCounter();

  @override
  String toString() => 'DecrementCounter';
}
📌 Nota: Usamos `Equatable` para poder comparar instancias de eventos y estados de forma sencilla. Sin `Equatable`, cada nueva instancia de `IncrementCounter` sería considerada diferente, incluso si sus propiedades son las mismas.

2. Definir Estados (States) 📊

El estado de nuestra aplicación es simplemente el valor actual del contador.

Crea un archivo counter_state.dart dentro de lib/counter:

part of 'counter_bloc.dart';

@immutable
class CounterState extends Equatable {
  final int counterValue;

  const CounterState(this.counterValue);

  @override
  List<Object> get props => [counterValue];

  CounterState copyWith({
    int? counterValue,
  }) {
    return CounterState(
      counterValue ?? this.counterValue,
    );
  }

  @override
  String toString() => 'CounterState(counterValue: $counterValue)';
}

El método copyWith es una práctica común para crear un nuevo estado basado en el anterior, modificando solo las propiedades necesarias.

3. Crear el BLoC (Business Logic Component) 🧠

Ahora, implementaremos la lógica que transforma los eventos en estados. Este será el corazón de nuestro contador BLoC.

Crea un archivo counter_bloc.dart dentro de lib/counter:

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

part 'counter_event.dart';
part 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<IncrementCounter>((event, emit) {
      emit(CounterState(state.counterValue + 1));
    });

    on<DecrementCounter>((event, emit) {
      emit(CounterState(state.counterValue - 1));
    });
  }
}
🔥 Importante: El constructor de `CounterBloc` llama a `super(const CounterState(0))`, estableciendo el estado inicial del contador en `0`. Los métodos `on` registran manejadores para eventos específicos. Dentro de ellos, usamos `emit()` para enviar un nuevo estado.

4. Integrar con la Interfaz de Usuario (UI) 🖼️

Finalmente, conectaremos nuestro BLoC con la UI. Usaremos BlocProvider para proporcionar el BLoC y BlocBuilder para reconstruir la UI cuando el estado cambie.

Modifica el archivo lib/main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_counter_app/counter/counter_bloc.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: MaterialApp(
        title: 'BLoC Counter App',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contador con BLoC')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'El valor actual del contador es:',
            ),
            BlocBuilder<CounterBloc, CounterState>(
              builder: (context, state) {
                return Text(
                  '${state.counterValue}',
                  style: Theme.of(context).textTheme.headlineMedium,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            heroTag: 'incrementBtn',
            onPressed: () => context.read<CounterBloc>().add(const IncrementCounter()),
            tooltip: 'Incrementar',
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 10),
          FloatingActionButton(
            heroTag: 'decrementBtn',
            onPressed: () => context.read<CounterBloc>().add(const DecrementCounter()),
            tooltip: 'Decrementar',
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
💡 Consejo: `context.read()` se usa para obtener una instancia del `CounterBloc` disponible en el árbol de widgets. Luego, llamamos a `.add()` para enviar un evento al BLoC. `BlocBuilder` es esencial para escuchar los cambios de estado y reconstruir solo la parte de la UI que depende de ese estado.

BlocListener para Efectos Secundarios 👂

En ocasiones, no queremos reconstruir la UI, sino realizar una acción de una sola vez en respuesta a un cambio de estado. Aquí es donde BlocListener entra en juego. Por ejemplo, mostrar un SnackBar o navegar a otra pantalla.

Vamos a añadir un BlocListener para mostrar un mensaje si el contador llega a 5 o -5.

Modifica CounterPage en main.dart:

// ... (código anterior)

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contador con BLoC')),
      body:
      BlocListener<CounterBloc, CounterState>(
        listener: (context, state) {
          if (state.counterValue == 5) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('¡Llegaste a 5! 🎉')),
            );
          } else if (state.counterValue == -5) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('¡Llegaste a -5! 🥶')),
            );
          }
        },
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'El valor actual del contador es:',
              ),
              BlocBuilder<CounterBloc, CounterState>(
                builder: (context, state) {
                  return Text(
                    '${state.counterValue}',
                    style: Theme.of(context).textTheme.headlineMedium,
                  );
                },
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            heroTag: 'incrementBtn',
            onPressed: () => context.read<CounterBloc>().add(const IncrementCounter()),
            tooltip: 'Incrementar',
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 10),
          FloatingActionButton(
            heroTag: 'decrementBtn',
            onPressed: () => context.read<CounterBloc>().add(const DecrementCounter()),
            tooltip: 'Decrementar',
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Ahora, cuando el contador alcance 5 o -5, verás un SnackBar en la parte inferior de la pantalla sin que se reconstruya todo el widget principal. BlocListener es excelente para efectos secundarios que no implican cambios directos en la UI mostrada.


BlocConsumer para Combinar Listener y Builder 🔄

Si necesitas tanto escuchar eventos para efectos secundarios como construir UI en respuesta a cambios de estado, puedes usar BlocConsumer. Este widget combina las funcionalidades de BlocListener y BlocBuilder.

Vamos a modificar el ejemplo para usar BlocConsumer:

// ... (imports y MyApp igual)

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contador con BLoC')),
      body: BlocConsumer<CounterBloc, CounterState>(
        listener: (context, state) {
          if (state.counterValue == 5) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('¡Llegaste a 5! 🎉')),
            );
          } else if (state.counterValue == -5) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('¡Llegaste a -5! 🥶')),
            );
          }
        },
        builder: (context, state) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'El valor actual del contador es:',
                ),
                Text(
                  '${state.counterValue}',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          );
        },
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            heroTag: 'incrementBtn',
            onPressed: () => context.read<CounterBloc>().add(const IncrementCounter()),
            tooltip: 'Incrementar',
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 10),
          FloatingActionButton(
            heroTag: 'decrementBtn',
            onPressed: () => context.read<CounterBloc>().add(const DecrementCounter()),
            tooltip: 'Decrementar',
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
⚠️ Advertencia: Usa `BlocConsumer` cuando **realmente necesites ambas funcionalidades**. Si solo necesitas construir la UI, usa `BlocBuilder`. Si solo necesitas escuchar efectos secundarios, usa `BlocListener`. Evitar el uso innecesario de `BlocConsumer` ayuda a mantener la claridad y el rendimiento.

Estructura de Carpetas Sugerida 📂

Para aplicaciones más grandes, una buena estructura de carpetas es crucial. Una estructura común para BLoC es organizar por características o módulos.

lib/
├── main.dart
├── app_config/
│   └── app_config.dart
├── common/
│   ├── widgets/
│   └── utils/
├── features/
│   ├── counter/
│   │   ├── bloc/
│   │   │   ├── counter_bloc.dart
│   │   │   ├── counter_event.dart
│   │   │   └── counter_state.dart
│   │   ├── models/
│   │   ├── views/
│   │   │   └── counter_page.dart
│   │   └── widgets/
│   ├── auth/
│   │   ├── bloc/
│   │   │   ├── auth_bloc.dart
│   │   │   ├── auth_event.dart
│   │   │   └── auth_state.dart
│   │   ├── data/
│   │   ├── models/
│   │   └── views/
│   └── settings/
│       └── ...
└── services/
    ├── api_service.dart
    └── auth_service.dart

Esta estructura ayuda a mantener la lógica de negocio, los eventos, los estados y las vistas agrupados por funcionalidad, facilitando la navegación y el mantenimiento.


Patrón Repository con BLoC (Más Avanzado) 🚀

Para aplicaciones reales, el BLoC interactuará con repositorios que se encargan de obtener datos (desde APIs, bases de datos locales, etc.). Esto añade otra capa de abstracción y mejora la separación de preocupaciones.

1. Crear un CounterRepository

Crea un archivo lib/counter/data/counter_repository.dart:

class CounterRepository {
  int _initialCounter = 0;

  Future<int> fetchInitialCounter() async {
    // Simula una llamada a API o base de datos
    await Future.delayed(const Duration(seconds: 1));
    return _initialCounter;
  }

  // Podríamos tener métodos para guardar el contador si fuera persistente
  Future<void> saveCounter(int value) async {
    // Simula guardar el contador
    await Future.delayed(const Duration(milliseconds: 500));
    _initialCounter = value; // Actualizar el valor en el repositorio
    print('Contador guardado: $value');
  }
}

2. Actualizar CounterEvent y CounterState para incluir carga

Modifica counter_event.dart:

// ... (código existente)

class LoadCounter extends CounterEvent {
  const LoadCounter();

  @override
  String toString() => 'LoadCounter';
}

Modifica counter_state.dart para manejar un estado de carga:

// ... (código existente)

enum CounterStatus { initial, loading, success, failure }

class CounterState extends Equatable {
  final int counterValue;
  final CounterStatus status;

  const CounterState(this.counterValue, {this.status = CounterStatus.initial});

  @override
  List<Object> get props => [counterValue, status];

  CounterState copyWith({
    int? counterValue,
    CounterStatus? status,
  }) {
    return CounterState(
      counterValue ?? this.counterValue,
      status: status ?? this.status,
    );
  }

  @override
  String toString() => 'CounterState(counterValue: $counterValue, status: $status)';
}

3. Modificar CounterBloc para usar el Repository

Modifica counter_bloc.dart para inyectar el repositorio y manejar el evento LoadCounter:

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:bloc_counter_app/counter/data/counter_repository.dart'; // Importa el repositorio

part 'counter_event.dart';
part 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  final CounterRepository _counterRepository; // Inyectar el repositorio

  CounterBloc(this._counterRepository) : super(const CounterState(0)) {
    on<IncrementCounter>((event, emit) {
      final newValue = state.counterValue + 1;
      emit(state.copyWith(counterValue: newValue));
      _counterRepository.saveCounter(newValue); // Guardar el nuevo valor
    });

    on<DecrementCounter>((event, emit) {
      final newValue = state.counterValue - 1;
      emit(state.copyWith(counterValue: newValue));
      _counterRepository.saveCounter(newValue); // Guardar el nuevo valor
    });

    on<LoadCounter>((event, emit) async {
      emit(state.copyWith(status: CounterStatus.loading));
      try {
        final initialValue = await _counterRepository.fetchInitialCounter();
        emit(state.copyWith(counterValue: initialValue, status: CounterStatus.success));
      } catch (_) {
        emit(state.copyWith(status: CounterStatus.failure));
      }
    });
  }
}

4. Actualizar main.dart para proporcionar el Repository y LoadCounter

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_counter_app/counter/counter_bloc.dart';
import 'package:bloc_counter_app/counter/data/counter_repository.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider(
      create: (context) => CounterRepository(),
      child: BlocProvider(
        create: (context) => CounterBloc(context.read<CounterRepository>())..add(const LoadCounter()), // Se inyecta el repositorio y se dispara el evento de carga
        child: MaterialApp(
          title: 'BLoC Counter App',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const CounterPage(),
        ),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contador con BLoC')),
      body: BlocConsumer<CounterBloc, CounterState>(
        listener: (context, state) {
          if (state.status == CounterStatus.failure) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Error al cargar el contador 😥')),
            );
          } else if (state.counterValue == 5) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('¡Llegaste a 5! 🎉')),
            );
          } else if (state.counterValue == -5) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('¡Llegaste a -5! 🥶')),
            );
          }
        },
        builder: (context, state) {
          if (state.status == CounterStatus.loading) {
            return const Center(child: CircularProgressIndicator());
          } else if (state.status == CounterStatus.failure) {
            return const Center(child: Text('Error: No se pudo cargar el contador.'));
          }
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'El valor actual del contador es:',
                ),
                Text(
                  '${state.counterValue}',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          );
        },
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            heroTag: 'incrementBtn',
            onPressed: () => context.read<CounterBloc>().add(const IncrementCounter()),
            tooltip: 'Incrementar',
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 10),
          FloatingActionButton(
            heroTag: 'decrementBtn',
            onPressed: () => context.read<CounterBloc>().add(const DecrementCounter()),
            tooltip: 'Decrementar',
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
📌 Nota: Usamos `RepositoryProvider` para hacer el `CounterRepository` disponible en el árbol de widgets, y luego lo inyectamos en `CounterBloc` usando `context.read()`. La línea `..add(const LoadCounter())` asegura que el BLoC intente cargar el contador inicial tan pronto como se crea.

Con esta implementación, la aplicación simulará una carga inicial de datos, mostrando un indicador de progreso, y manejará posibles errores de carga. Además, cada vez que el contador cambia, se "guardará" el nuevo valor en el repositorio simulado.


Pruebas de BLoC (Unit Testing) ✅

Una de las grandes ventajas de BLoC es su facilidad para ser probado. Como la lógica de negocio está aislada, podemos probar los BLoCs de forma independiente.

Para esto, necesitaremos la dependencia bloc_test.

dev_dependencies:
  flutter_test:
    sdk: flutter
  bloc_test: ^9.1.5 # Añade esta dependencia
  mocktail: ^1.0.0 # Para crear mocks de repositorios

flutter pub get

Crea un archivo test/counter/counter_bloc_test.dart:

import 'package:bloc_counter_app/counter/counter_bloc.dart';
import 'package:bloc_counter_app/counter/data/counter_repository.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

// Mock del repositorio para aislar el BLoC en las pruebas
class MockCounterRepository extends Mock implements CounterRepository {}

void main() {
  group('CounterBloc', () {
    late MockCounterRepository mockCounterRepository;

    setUp(() {
      mockCounterRepository = MockCounterRepository();
      // Mockear la respuesta inicial del repositorio
      when(() => mockCounterRepository.fetchInitialCounter())
          .thenAnswer((_) async => 0);
      // Mockear el método saveCounter
      when(() => mockCounterRepository.saveCounter(any()))
          .thenAnswer((_) async => Future.value());
    });

    blocTest<
        CounterBloc,
        CounterState>('emits [CounterState(0, status: success)] when LoadCounter is added',
        build: () => CounterBloc(mockCounterRepository),
        act: (bloc) => bloc.add(const LoadCounter()),
        expect: () => <CounterState>[
              const CounterState(0, status: CounterStatus.loading),
              const CounterState(0, status: CounterStatus.success),
            ],
    );

    blocTest<
        CounterBloc,
        CounterState>('emits [CounterState(1)] when IncrementCounter is added',
        build: () => CounterBloc(mockCounterRepository),
        seed: () => const CounterState(0, status: CounterStatus.success),
        act: (bloc) => bloc.add(const IncrementCounter()),
        expect: () => <CounterState>[
              const CounterState(1, status: CounterStatus.success),
            ],
    );

    blocTest<
        CounterBloc,
        CounterState>('emits [CounterState(-1)] when DecrementCounter is added',
        build: () => CounterBloc(mockCounterRepository),
        seed: () => const CounterState(0, status: CounterStatus.success),
        act: (bloc) => bloc.add(const DecrementCounter()),
        expect: () => <CounterState>[
              const CounterState(-1, status: CounterStatus.success),
            ],
    );

    blocTest<
        CounterBloc,
        CounterState>('emits [CounterState(0, status: failure)] when LoadCounter fails',
        build: () {
          when(() => mockCounterRepository.fetchInitialCounter())
              .thenThrow(Exception('Error de carga')); // Simular un error
          return CounterBloc(mockCounterRepository);
        },
        act: (bloc) => bloc.add(const LoadCounter()),
        expect: () => <CounterState>[
              const CounterState(0, status: CounterStatus.loading),
              const CounterState(0, status: CounterStatus.failure),
            ],
    );

  });
}

Ejecuta las pruebas con flutter test test/counter/counter_bloc_test.dart.

💡 Consejo: `bloc_test` proporciona una API muy declarativa para probar BLoCs. `build` crea el BLoC, `seed` establece un estado inicial (opcional), `act` añade eventos y `expect` define la secuencia de estados esperados.

Consideraciones Adicionales y Buenas Prácticas ✨

  • Uso de Cubit vs Bloc: Cubit es una versión simplificada de BLoC, ideal para estados más simples que no requieren manejar eventos de manera explícita (solo funciones que emiten estados). Para lógicas más complejas con muchos eventos y estados, Bloc es más adecuado. Siempre puedes refactorizar de Cubit a Bloc si la complejidad crece.
  • BlocObserver: Para depuración, puedes usar BlocObserver para observar todos los cambios de estado y eventos en tu aplicación. Esto es muy útil para entender el flujo de datos.
// En main.dart
import 'package:bloc/bloc.dart';

class SimpleBlocObserver extends BlocObserver {
@override
void onEvent(Bloc bloc, Object? event) {
super.onEvent(bloc, event);
print('onEvent ${bloc.runtimeType} $event');
}

@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('onChange ${bloc.runtimeType} $change');
}

@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('onError ${bloc.runtimeType} $error $stackTrace');
super.onError(bloc, error, stackTrace);
}

@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('onTransition ${bloc.runtimeType} $transition');
}
}

void main() {
Bloc.observer = SimpleBlocObserver(); // Añadir al inicio de main
runApp(const MyApp());
}
  • Manejo de estados complejos: Para estados con múltiples propiedades, usa el patrón copyWith en tus clases de estado (Equatable te ayudará mucho aquí) para crear nuevos estados inmutables de manera eficiente.
  • MultiBlocProvider y MultiRepositoryProvider: Si tu aplicación tiene múltiples BLoCs o repositorios, puedes usar estos widgets para proveerlos de manera limpia en el árbol de widgets.
¿Por qué la inmutabilidad es importante en BLoC?La inmutabilidad de los estados asegura que cada vez que un estado cambia, se crea una nueva instancia de estado. Esto permite a `BlocBuilder` y `BlocListener` detectar cambios de forma fiable y optimizar las reconstrucciones de la UI. Si mutas el estado existente, estos widgets no sabrán que ha habido un cambio.
Tutorial Completado

¡Felicidades! Has completado un recorrido exhaustivo por la gestión de estado con BLoC en Flutter. Desde los fundamentos hasta la implementación con un repositorio y las pruebas unitarias, ahora tienes las herramientas para construir aplicaciones Flutter robustas y escalables con BLoC.

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!