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๊ฐ ์๋ค. ์ด๋ค์ ์ฐจ์ด๋ ๋ค์๊ณผ ๊ฐ๋ค.
BlocBuilder
BlocBuilder๋ UI๋ฅผ ํ๋ฉด์ ๊ทธ๋ฆด ๋ ์ฌ์ฉ๋๋ ์์ ฏ ์ค ํ๋๋ก, BlocProvider๋ก๋ถํฐ Bloc์ ๊ฐ์ ธ์์ ํ๋ฉด์ ๊ทธ๋ฆฌ๊ธฐ ์ํ Builder ํจ์๋ฅผ ํธ์ถํ๋ค. Builder ํจ์๋ ํ์ฌ ์ํ์ ๋ฐ๋ผ UI๋ฅผ ์์ฑํ๋๋ฐ, ์ด UI๋ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์๋ก ๊ทธ๋ ค์ง๋ค. ๋ฐ๋ผ์ BlocBuilder๋ Bloc์ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค UI๋ฅผ ๊ฐฑ์ ํ๋๋ฐ ์ฌ์ฉ๋๋ค.
BlocConsumer
BlocConsumer๋ BlocBuilder์ ์ ์ฌํ์ง๋ง, UI๋ฅผ ๊ทธ๋ฆฌ๋ Builder ํจ์๋ฟ๋ง ์๋๋ผ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์ํํ ์ด๋ฒคํธ๋ ๋ฑ๋กํ ์ ์๋ค. ์ฆ, BlocBuilder์ ๋ฌ๋ฆฌ ์ํ ๋ณ๊ฒฝ ์ด๋ฒคํธ์ ๋ฐ๋ฅธ ๋์์ ์ฒ๋ฆฌํ ์ ์๋ค.
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