CH02 Bloc Overview

๋‹ค์ˆ˜์˜ Bloc ์„ ๊ตฌ๋…ํ•˜์—ฌ ์ด ๊ฒฐ๊ณผ๋“ค์„ ๋ณตํ•ฉ์ ์œผ๋กœ ์ฐธ์กฐํ•˜์—ฌ ์ƒˆ๋กœ์šด ๊ฒฐ๊ณผ๋ฅผ ๋งŒ๋“ค์–ด์•ผํ•  ๊ฒฝ์šฐ ์•„๋ž˜์™€ ๊ฐ™์ด MultiBlocListener๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

import 'package:bloc_sample_todo_app/blocs/filtered_todos/filtered_todos_bloc.dart';
import 'package:bloc_sample_todo_app/blocs/search_term/search_term_bloc.dart';
import 'package:bloc_sample_todo_app/blocs/selected_filter/selected_filter_bloc.dart';
import 'package:bloc_sample_todo_app/blocs/todos/todos_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../widgets/widgets.dart';

class TodoList extends StatefulWidget {
  const TodoList({Key? key}) : super(key: key);

  @override
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  @override
  Widget build(BuildContext context) {
    return MultiBlocListener(
      listeners: [
        BlocListener<TodosBloc, TodosState>(listener: (context, state) {
          context.read<FilteredTodosBloc>().add(CalculateFilteredTodosEvent(
                currentTodos: state.todos,
                selectedFilter:
                    context.read<SelectedFilterBloc>().state.selectedFilter,
                searchTerm: context.read<SearchTermBloc>().state.searchTerm,
              ));
        }),
        BlocListener<SearchTermBloc, SearchTermState>(
            listener: (context, state) {
          context.read<FilteredTodosBloc>().add(CalculateFilteredTodosEvent(
                currentTodos: context.read<TodosBloc>().state.todos,
                selectedFilter:
                    context.read<SelectedFilterBloc>().state.selectedFilter,
                searchTerm: state.searchTerm,
              ));
        }),
        BlocListener<SelectedFilterBloc, SelectedFilterState>(
            listener: (context, state) {
          context.read<FilteredTodosBloc>().add(CalculateFilteredTodosEvent(
                currentTodos: context.read<TodosBloc>().state.todos,
                selectedFilter: state.selectedFilter,
                searchTerm: context.read<SearchTermBloc>().state.searchTerm,
              ));
        }),
      ],
      child: BlocBuilder<FilteredTodosBloc, FilteredTodosState>(
        builder: (context, state) {
          return ListView.separated(
            primary: false,
            shrinkWrap: true,
            separatorBuilder: (_, __) {
              return const Divider(color: Colors.grey);
            },
            itemCount: state.filteredTodos.length,
            itemBuilder: (_, index) =>
                TodoItem(todo: state.filteredTodos[index]),
          );
        },
      ),
    );
  }
}
import 'package:bloc/bloc.dart';
import 'package:bloc_sample_todo_app/domain/models/models.dart';
import 'package:bloc_sample_todo_app/enums/enums.dart';
import 'package:equatable/equatable.dart';

part 'filtered_todos_event.dart';
part 'filtered_todos_state.dart';

class FilteredTodosBloc extends Bloc<FilteredTodosEvent, FilteredTodosState> {
  FilteredTodosBloc() : super(FilteredTodosState.initial()) {
    on<CalculateFilteredTodosEvent>((event, emit) {
      final List<TodoModel> _currentTodos = event.currentTodos;
      final FilterType _selectedFilter = event.selectedFilter;
      final String _searchTerm = event.searchTerm;

      List<TodoModel> _filteredTodos;

      switch (_selectedFilter) {
        case FilterType.active:
          _filteredTodos = _currentTodos
              .where((TodoModel todo) => !todo.isCompleted)
              .toList();
          break;
        case FilterType.completed:
          _filteredTodos = _currentTodos
              .where((TodoModel todo) => todo.isCompleted)
              .toList();
          break;
        case FilterType.all:
        default:
          _filteredTodos = _currentTodos;
          break;
      }

      if (_searchTerm.isNotEmpty) {
        _filteredTodos = _filteredTodos
            .where((TodoModel todo) => todo.description
                .toLowerCase()
                .contains(_searchTerm.toLowerCase()))
            .toList();
      }

      emit(state.copyWith(filteredTodos: _filteredTodos));
    });
  }
}

๋‹ค์Œ์€ BlocBuilder, BlocConsumer, BlocListener ์˜ ์ฐจ์ด์— ๊ด€ํ•œ ๋‚ด์šฉ์ด๋‹ค.

Flutter์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ Bloc ํŒจํ„ด์—์„œ, UI์™€ ์ƒํƒœ๊ด€๋ฆฌ ๋ธ”๋ก(Bloc) ๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ๋•๋Š” ์œ„์ ฏ๋“ค์—๋Š” BlocBuilder, BlocConsumer, BlocListener๊ฐ€ ์žˆ๋‹ค. ์ด๋“ค์˜ ์ฐจ์ด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. BlocBuilder

BlocBuilder๋Š” UI๋ฅผ ํ™”๋ฉด์— ๊ทธ๋ฆด ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ์œ„์ ฏ ์ค‘ ํ•˜๋‚˜๋กœ, BlocProvider๋กœ๋ถ€ํ„ฐ Bloc์„ ๊ฐ€์ ธ์™€์„œ ํ™”๋ฉด์— ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ Builder ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. Builder ํ•จ์ˆ˜๋Š” ํ˜„์žฌ ์ƒํƒœ์— ๋”ฐ๋ผ UI๋ฅผ ์ƒ์„ฑํ•˜๋Š”๋ฐ, ์ด UI๋Š” ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ ๊ทธ๋ ค์ง„๋‹ค. ๋”ฐ๋ผ์„œ BlocBuilder๋Š” Bloc์˜ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค UI๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

  1. BlocConsumer

BlocConsumer๋Š” BlocBuilder์™€ ์œ ์‚ฌํ•˜์ง€๋งŒ, UI๋ฅผ ๊ทธ๋ฆฌ๋Š” Builder ํ•จ์ˆ˜๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ˆ˜ํ–‰ํ•  ์ด๋ฒคํŠธ๋„ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰, BlocBuilder์™€ ๋‹ฌ๋ฆฌ ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ์— ๋”ฐ๋ฅธ ๋™์ž‘์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

  1. BlocListener

BlocListener๋Š” Bloc์˜ ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜๋Š” ์œ„์ ฏ์œผ๋กœ, BlocBuilder๋‚˜ BlocConsumer์™€ ๋‹ฌ๋ฆฌ UI๋ฅผ ๊ทธ๋ฆฌ์ง€๋Š” ์•Š๋Š”๋‹ค. ๋Œ€์‹ , Bloc์—์„œ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ˆ˜ํ–‰ํ•  ์ด๋ฒคํŠธ๋ฅผ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋‹ค. BlocListener๋Š” Bloc์ด ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ™”๋ฉด์— Toast ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ๋“ฑ์˜ ๋™์ž‘์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฆ‰, BlocBuilder๋Š” ์ƒํƒœ๋ฅผ ๋ฐ”๋ผ๋ณด๊ณ , BlocConsumer๋Š” ์ƒํƒœ๋ฅผ ๋ฐ”๋ผ๋ณด๋ฉด์„œ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ , BlocListener๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋ฐ”๋ผ๋ณธ๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋“ค์€ ๊ฐ๊ฐ์˜ ์ƒํ™ฉ์—์„œ ํ•„์š”์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์œ„์ ฏ์„ ์„ ํƒํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

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

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

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState.initial()) {
    // on<IncrementCounterEvent>(
    //   _handleIncrementCounterEvent,
    //   transformer: sequential(),
    // );

    // on<DecrementCounterEvent>(
    //   _handleDecrementCounterEvent,
    //   transformer: sequential(),
    // );

    on<CounterEvent>(
      (event, emit) async {
        if (event is IncrementCounterEvent) {
          await _handleIncrementCounterEvent(event, emit);
        } else if (event is DecrementCounterEvent) {
          await _handleDecrementCounterEvent(event, emit);
        }
      },
      transformer: sequential(),
    );
  }

  Future<void> _handleIncrementCounterEvent(event, emit) async {
    await Future.delayed(Duration(seconds: 4));
    emit(state.copyWith(counter: state.counter + 1));
  }

  Future<void> _handleDecrementCounterEvent(event, emit) async {
    await Future.delayed(Duration(seconds: 2));
    emit(state.copyWith(counter: state.counter - 1));
  }
}

Bloc ์—์„œ ์–ธ์ œ state ๊ฐ€ ๋ฐ”๋€Œ๋‚˜

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด emit ๊ณผ์ •์—์„œ state ๋ฅผ ๊ฐˆ์•„์น˜์šด๋‹ค. (๊ธฐ์กด state์™€ ๋น„๊ตํ•˜์—ฌ ๋‹ฌ๋ผ์ง„ state ๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ์—๋งŒ) Bloc<Event, State>๊ฐ€ BlocBase<State>๋ฅผ ์ƒ์†ํ•˜๊ณ  ์žˆ๊ณ  BlocBase<State>๊ฐ€ state๋ฅผ ํ•„๋“œ๋กœ ๊ฐ–๊ณ  ์žˆ๋Š” ์ƒํƒœ์ด๋‹ค.

์ฝ”๋“œ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋“ฏ์ด Bloc<Event, State>์˜ emit ์€ ๊ฒฐ๊ตญ super.emit(state)๋กœ ๊ฒฐ๊ตญ BlocBase<State>์˜ emit ์„ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด๋ฉฐ ์—ฌ๊ธฐ์„œ ํ•„๋“œ๋กœ ๊ด€๋ฆฌ์ค‘์ธ ๊ธฐ์กด state ์™€ ๋น„๊ต ํ›„ ๋‹ค๋ฅด๋ฉด ์ƒˆ๋กœ์šด state ์„ ํ• ๋‹นํ•ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.

abstract class Bloc<Event, State> extends BlocBase<State>
    implements BlocEventSink<Event> {
  
  ...
      
  @visibleForTesting
  @override
  void emit(State state) => super.emit(state);

  ...
    
  }
abstract class BlocBase<State>
    implements StateStreamableSource<State>, Emittable<State>, ErrorSink {
 
 ...
 
   /// Updates the [state] to the provided [state].
  /// [emit] does nothing if the [state] being emitted
  /// is equal to the current [state].
  ///
  /// To allow for the possibility of notifying listeners of the initial state,
  /// emitting a state which is equal to the initial state is allowed as long
  /// as it is the first thing emitted by the instance.
  ///
  /// * Throws a [StateError] if the bloc is closed.
  @protected
  @visibleForTesting
  @override
  void emit(State state) {
    try {
      if (isClosed) {
        throw StateError('Cannot emit new states after calling close');
      }
      if (state == _state && _emitted) return;
      onChange(Change<State>(currentState: this.state, nextState: state));
      _state = state;
      _stateController.add(_state);
      _emitted = true;
    } catch (error, stackTrace) {
      onError(error, stackTrace);
      rethrow;
    }
  }

...
       
}    

Bloc ์—์„œ ์–ธ์ œ ๊ตฌ๋… ์œ„์ ฏ์ด state ๋ณ€ํ™”๋ฅผ ์•Œ๊ฒŒ ๋˜๋Š”๊ฐ€

state ๋ณ€ํ™”์— ๋Œ€ํ•ด์„œ ์ด๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ์œ„์ ฏ์— ๋ณ€ํ™”๋ฅผ ์•Œ๋ฆฌ๋Š” ๊ฒƒ์€ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก ๋ถ€๋ถ„์—์„œ ๋ฐœ์ƒํ•œ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ๋ณด์ž.

  /// Register event handler for an event of type `E`.
  /// There should only ever be one event handler per event type `E`.
  ///
  /// ```dart
  /// abstract class CounterEvent {}
  /// class CounterIncrementPressed extends CounterEvent {}
  ///
  /// class CounterBloc extends Bloc<CounterEvent, int> {
  ///   CounterBloc() : super(0) {
  ///     on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  ///   }
  /// }
  /// ```
  ///
  /// * A [StateError] will be thrown if there are multiple event handlers
  /// registered for the same type `E`.
  ///
  /// By default, events will be processed concurrently.
  ///
  /// See also:
  ///
  /// * [EventTransformer] to customize how events are processed.
  /// * [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) for an
  /// opinionated set of event transformers.
  ///
  void on<E extends Event>(
    EventHandler<E, State> handler, {
    EventTransformer<E>? transformer,
  }) {
    assert(() {
      final handlerExists = _handlers.any((handler) => handler.type == E);
      if (handlerExists) {
        throw StateError(
          'on<$E> was called multiple times. '
          'There should only be a single event handler per event type.',
        );
      }
      _handlers.add(_Handler(isType: (dynamic e) => e is E, type: E));
      return true;
    }());

    final _transformer = transformer ?? _eventTransformer;
    final subscription = _transformer(
      _eventController.stream.where((event) => event is E).cast<E>(),
      (dynamic event) {
        void onEmit(State state) {
          if (isClosed) return;
          if (this.state == state && _emitted) return;
          onTransition(Transition(
            currentState: this.state,
            event: event as E,
            nextState: state,
          ));
          emit(state);
        }

        final emitter = _Emitter(onEmit);
        final controller = StreamController<E>.broadcast(
          sync: true,
          onCancel: emitter.cancel,
        );

        void handleEvent() async {
          void onDone() {
            emitter.complete();
            _emitters.remove(emitter);
            if (!controller.isClosed) controller.close();
          }

          try {
            _emitters.add(emitter);
            await handler(event as E, emitter);
          } catch (error, stackTrace) {
            onError(error, stackTrace);
            rethrow;
          } finally {
            onDone();
          }
        }

        handleEvent();
        return controller.stream;
      },
    ).listen(null);
    _subscriptions.add(subscription);
  }

Last updated