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