컨텐츠로 건너뛰기

자주 묻는 질문

State가 업데이트되지 않아요

Question: Bloc에서 state를 emit하는데 UI가 업데이트되지 않아요. 무엇이 문제인가요?

💡 Answer: 만약 Equatable을 사용하는 경우 모든 프로퍼티를 props getter에 전달해야 합니다.

GOOD

my_state.dart
sealed class MyState extends Equatable {
const MyState();
}
final class StateA extends MyState {
final String property;
const StateA(this.property);
@override
List<Object> get props => [property]; // pass all properties to props
}

BAD

my_state.dart
sealed class MyState extends Equatable {
const MyState();
}
final class StateA extends MyState {
final String property;
const StateA(this.property);
@override
List<Object> get props => [];
}
my_state.dart
sealed class MyState extends Equatable {
const MyState();
}
final class StateA extends MyState {
final String property;
const StateA(this.property);
@override
List<Object> get props => null;
}

또한, Bloc에서 state의 새 인스턴스를 emit하고 있는지 확인하세요.

GOOD

my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
// always create a new instance of the state you are going to yield
emit(state.copyWith(property: event.property));
});
}
my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
final data = _getData(event.info);
// always create a new instance of the state you are going to yield
emit(MyState(data: data));
});
}

BAD

my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
// never modify/mutate state
state.property = event.property;
// never emit the same instance of state
emit(state);
});
}

언제 Equatable를 사용해야 하나요

Question: Equatable은 언제 사용해야 하나요?

💡Answer:

my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
emit(StateA('hi'));
emit(StateA('hi'));
});
}

위의 시나리오에서 StateAEquatable을 extends 한다면 하나의 state 변경만 발생합니다 (두 번째 emit은 무시됩니다). 일반적으로 코드를 최적화하여 리빌드 횟수를 줄이려면 Equatable을 사용해야 합니다. 동일한 state가 연속적으로 여러 Transition을 촉발하려면 Equatable을 사용하면 안 됩니다.

또한, Equatable을 사용하면 MatchersPredicates를 사용하는 것보다 Bloc state의 특정 인스턴스를 예상할 수 있으므로 Bloc을 훨씬 쉽게 테스트할 수 있습니다.

my_bloc_test.dart
blocTest(
'...',
build: () => MyBloc(),
act: (bloc) => bloc.add(MyEvent()),
expect: [
MyStateA(),
MyStateB(),
],
);

Equatable이 없다면 위의 테스트는 실패할 것이고, 다음과 같이 다시 작성해야 합니다:

my_bloc_test.dart
blocTest(
'...',
build: () => MyBloc(),
act: (bloc) => bloc.add(MyEvent()),
expect: [
isA<MyStateA>(),
isA<MyStateB>(),
],
);

에러 처리

Question: 이전 데이터를 계속 표시하면서 에러를 처리하려면 어떻게 해야 하나요?

💡 Answer:

이는 Bloc의 state가 어떻게 모델링되었는지에 따라 크게 달라집니다. 에러가 발생하더라도 데이터를 계속 유지해야 하는 경우에는 단일 state 클래스를 사용하는 것이 좋습니다.

my_state.dart
enum Status { initial, loading, success, failure }
class MyState {
const MyState({
this.data = Data.empty,
this.error = '',
this.status = Status.initial,
});
final Data data;
final String error;
final Status status;
MyState copyWith({Data data, String error, Status status}) {
return MyState(
data: data ?? this.data,
error: error ?? this.error,
status: status ?? this.status,
);
}
}

이렇게 하면 위젯이 데이터에러 프로퍼티에 동시에 접근할 수 있으며, Bloc은 state.copyWith을 사용하여 에러가 발생한 경우에도 이전 데이터를 유지할 수 있습니다.

my_bloc.dart
on<DataRequested>((event, emit) {
try {
final data = await _repository.getData();
emit(state.copyWith(status: Status.success, data: data));
} catch(error) {
emit(state.copyWith(status: Status.failure, error: 'Something went wrong!'));
}
});

Bloc vs. Redux

Question: Bloc과 Redux의 차이점은 무엇인가요?

💡 Answer:

BLoC 은 다음 규칙에 의해 정의되는 디자인 패턴입니다:

  1. BLoC의 입력과 출력은 간단한 Streams과 Sinks 입니다.
  2. 종속성은 주입이 가능하고 플렛폼에 구애받지 않아야 합니다.
  3. 플랫폼별 분기은 허용되지 않습니다.
  4. 위의 규칙을 따르는 한 원하는 대로 구현할 수 있습니다.

UI 가이드라인은 다음과 같습니다:

  1. “충분히 복잡한” 각 컴포넌트에는 해당하는 BLoC이 있습니다.
  2. 컴포넌트는 입력을 “있는 그대로” 보내야 합니다.
  3. 컴포넌트는 가능한 “있는 그대로”에 가까운 출력을 표시해야 합니다.
  4. 모든 분기는 간단한 BLoC boolean 출력을 기반으로 해야 합니다.

Bloc 라이브러리는 BLoC 디자인 패턴을 구현하며 개발자 경험을 단순화하기 위해 RxDart를 추상화하는 것을 목표로 합니다.

Redux의 세 원칙은 다음과 같습니다:

  1. 신뢰할 수 있는 단일 소스
  2. State는 읽기 전용
  3. 순수 함수로 Change가 이루어짐

Bloc 라이브러리는 bloc state가 여러 bloc에 분산되어 있기 때문에 첫 번째 원칙을 위반합니다. 또한 bloc에는 미들웨어라는 개념이 없으며 bloc은 비동기 상태 변경을 매우 쉽게 할 수 있도록 설계되어 단일 event에 대해 여러 state를 emit할 수 있습니다.

Bloc vs. Provider

Question: Bloc과 Provider의 차이점은 무엇인가요?

💡 Answer: provider는 종속성 주입을 위해 설계되었습니다 (InheritedWidget을 래핑합니다). 여전히 state를 관리하는 방법을 알아내야 합니다 (ChangeNotifier, Bloc, Mobx 등을 통해). Bloc 라이브러리는 내부적으로 provider를 사용하여 위젯 트리 전체에서 bloc을 쉽게 제공하고 접근할 수 있도록 합니다.

BlocProvider.of()가 Bloc을 못 찾아요

Question: BlocProvider.of(context)을 사용할 때 bloc을 찾을 수 없어요. 어떻게 고치면 될까요?

💡 Answer: Bloc이 제공한 context와 동일한 context에서는 bloc에 접근할 수 없으므로, 하위 BuildContext 내에서 BlocProvider.of()가 호출되는지 확인해야 합니다.

GOOD

my_widget.dart
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BlocA(),
child: MyChild();
);
}
class MyChild extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
final blocA = BlocProvider.of<BlocA>(context);
...
},
)
...
}
}
my_widget.dart
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BlocA(),
child: Builder(
builder: (context) => ElevatedButton(
onPressed: () {
final blocA = BlocProvider.of<BlocA>(context);
...
},
),
),
);
}

BAD

my_widget.dart
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BlocA(),
child: ElevatedButton(
onPressed: () {
final blocA = BlocProvider.of<BlocA>(context);
...
}
)
);
}

프로젝트 구조

Question: 프로젝트를 어떻게 구조화하는게 좋을까요?

💡 Answer: 이 질문에 대한 정답은 없지만, 몇 가지 권장되는 참고 자료는 다음과 같습니다:

가장 중요한 것은 일관성있고 의도적인 프로젝트 구조를 갖는 것입니다.

Bloc 내에서 Event 추가하기

Question: Bloc 내에서 event를 추가해도 괜찮은가요?

💡 Answer: 대부분의 경우, event는 외부에서 추가해야 하지만 일부 경우에는 event를 내부적으로 추가하는 것이 합리적일 수 있습니다.

내부 event가 사용되는 가장 일반적인 상황은 Repository의 실시간 업데이트에 대한 응답으로 state의 변경이 발생해야 하는 경우입니다. 이러한 상황에서 Repository는 버튼 탭과 같은 외부 event 대신 state 변경에 대한 자극이 됩니다.

다음 예시에서 MyBloc의 state는 UserRepositoryStream<User>를 통해 노출되는 현재 사용자에 따라 달라집니다. MyBloc은 현재 사용자의 변경 사항을 수신하고, 사용자가 사용자 stream에서 방출될 때 마다 내부 _UserChanged event를 추가합니다.

my_bloc.dart
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc({required UserRepository userRepository}) : super(...) {
on<_UserChanged>(_onUserChanged);
_userSubscription = userRepository.user.listen(
(user) => add(_UserChanged(user)),
);
}
}

내부 event를 추가함으로써 event에 대한 커스텀 transformer를 지정하여 여러 _UserChanged event가 처리되는 방식을 결정할 수도 있습니다. 기본적으로 event는 동시에 처리됩니다.

내부 event는 private로 정의되는 것을 강력히 권장합니다. 이는 특정 event가 bloc 자체 내에서만 사용한다는 것을 명시적으로 알리는 방법이며, 외부 컴포넌트가 event에 대해 아는 것을 방지합니다.

my_event.dart
sealed class MyEvent {}
// `EventA` is an external event.
final class EventA extends MyEvent {}
// `EventB` is an internal event.
// We are explicitly making `EventB` private so that it can only be used
// within the bloc.
final class _EventB extends MyEvent {}

또한 외부 Started event를 정의하고 emit.forEach API를 사용하여 실시간 사용자 업데이트에 대한 반응을 처리할 수 있습니다.

my_bloc.dart
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc({required UserRepository userRepository})
: _userRepository = userRepository, super(...) {
on<Started>(_onStarted);
}
Future<void> _onStarted(Started event, Emitter<MyState> emit) {
return emit.forEach(
_userRepository.user,
onData: (user) => MyState(...)
);
}
}

위 접근 방식의 장점은 다음과 같습니다:

  • 내부 _UserChanged event가 필요하지 않습니다.
  • StreamSubscription을 수동으로 관리할 필요가 없습니다.
  • Bloc이 사용자 업데이트 steram을 구독하는 시기를 완전히 제어할 수 있습니다.

위 접근 방식의 단점은 다음과 같습니다:

  • 구독을 쉽게 pause 하거나 resume할 수 없습니다.
  • 외부적으로 추가해야 하는 공개 Started event를 노출해야 합니다.
  • 사용자 업데이트에 반응하는 방식을 조정하기 위해 커스텀 transformer를 사용할 수 없습니다.

Public 메서드 노출

Question: Bloc 및 Cubit 인스턴스에 public 메서드를 노출해도 괜찮을까요?

💡 Answer

Cubit을 생성할 때 state 변경을 촉발할 목적으로만 public 메서드를 노출하는 것이 좋습니다. 결과적으로, 일반적인 cubit 인스턴스의 모든 public 메서드는 void 또는 Future<void>를 반환해야 합니다.

Bloc을 생성할 때 커스텀 public 메서드를 노출하지 않고, 대신 add를 호출하여 event를 bloc에 알리는 것이 좋습니다.