Sparround

Bloc: on<Event>, emit and handler rules

Bloc is, in the docs' words, a more advanced class which relies on events to trigger state changes rather than functions. The structure:

  • Event classes are declared (usually a sealed class plus subclasses).
  • A handler is registered per event type in the constructor: on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  • The UI adds events: context.read<CounterBloc>().add(CounterIncrementPressed()).

Practical rules about handlers:

  • One handler per event type; registering a second on<T> for the same type is a mistake.
  • emit works inside the handler, including after an await; but you must not keep the emit and use it after the handler completes.
  • To turn a stream into state there are emit.forEach and emit.onEach — the subscription is managed together with the handler.
dart
// Event-lər keçmiş zamanda adlandırılır (rəsmi konvensiya).
sealed class TodoEvent {}

final class TodoStarted extends TodoEvent {}

final class TodoAdded extends TodoEvent {
  TodoAdded(this.title);
  final String title;
}

final class TodoDeleted extends TodoEvent {
  TodoDeleted(this.id);
  final String id;
}

class TodoBloc extends Bloc<TodoEvent, TodoState> {
  TodoBloc(this._repository) : super(const TodoState()) {
    on<TodoStarted>(_onStarted);
    on<TodoAdded>(_onAdded);
    on<TodoDeleted>(_onDeleted);
  }

  final TodoRepository _repository;

  Future<void> _onStarted(TodoStarted event, Emitter<TodoState> emit) async {
    emit(state.copyWith(status: TodoStatus.loading));
    try {
      final todos = await _repository.fetchAll();
      emit(state.copyWith(status: TodoStatus.success, todos: todos));
    } catch (error) {
      emit(state.copyWith(status: TodoStatus.failure, error: '$error'));
    }
  }

  Future<void> _onAdded(TodoAdded event, Emitter<TodoState> emit) async {
    await _repository.create(event.title);
    add(TodoStarted()); // yenidən yükləmə üçün event əlavə etmək məqbul yanaşmadır
  }

  Future<void> _onDeleted(TodoDeleted event, Emitter<TodoState> emit) async {
    emit(state.copyWith(
      todos: state.todos.where((t) => t.id != event.id).toList(),
    ));
    await _repository.remove(event.id);
  }
}

Sealed events, handlers and async work.

dart
class ChatBloc extends Bloc<ChatEvent, ChatState> {
  ChatBloc(this._repository) : super(const ChatState()) {
    on<ChatSubscriptionRequested>(_onSubscriptionRequested);
  }

  final ChatRepository _repository;

  Future<void> _onSubscriptionRequested(
    ChatSubscriptionRequested event,
    Emitter<ChatState> emit,
  ) async {
    // Stream-in hər elementi state-ə çevrilir; abunəlik handler ilə
    // birlikdə bağlanır — əl ilə StreamSubscription saxlamağa ehtiyac yoxdur.
    await emit.forEach<List<Message>>(
      _repository.watchMessages(),
      onData: (messages) => state.copyWith(messages: messages),
      onError: (error, stackTrace) => state.copyWith(error: '$error'),
    );
  }
}

`emit.forEach` — turning a stream into state.

The official naming conventions (especially recommended for large teams):

  • Events are named in the past tense, because from the bloc's perspective they have already happened: BlocSubject + noun (optional) + verb. The initial load event: BlocSubject + Started. The base event class: BlocSubject + Event.
  • States are nouns, because a state is a snapshot at a point in time. When represented as subclasses: BlocSubject + verb + State, where State is one of Initial, Success, Failure or InProgress.

📚 Sources and documentation