: 프레젠테이션과 비즈니스 로직을 쉽게 구분할 수 있어 코드가 빠르고, 테스트하기 쉽고, 재사용이 가능해짐
3개의 레이어로 구분한다.
: Bloc State에 따라 유저에게 어떻게 보여줄지를 결정한다.
: Bloc layer의 역할은 Presentation Layer으로 부터 발생한 event를 새로운 State로 바꾼다.
class MyBloc extends Bloc {
final OtherBloc otherBloc;
late final StreamSubscription otherBlocSubscription;
MyBloc(this.otherBloc) {
otherBlocSubscription = otherBloc.stream.listen((state) {
// React to state changes here.
// Add events here to trigger changes in MyBloc.
});
}
Future<void> close() {
otherBlocSubscription.cancel();
return super.close();
}
}
: 소스로부터 데이터를 찾고 조절함.
가장 낮은 Layer로 데이터베이스, 네트워크 요청, 비동기 데이터 소스와 상호작용함
BlocSubject
+ Noun (optional)
+ Verb (event)
sealed class CounterEvent {}
final class CounterStarted extends CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}
final class CounterIncrementRetried extends CounterEvent {}
sealed class CounterEvent {}
final class Initial extends CounterEvent {}
final class CounterInitialized extends CounterEvent {}
final class Increment extends CounterEvent {}
final class DoIncrement extends CounterEvent {}
final class IncrementCounter extends CounterEvent {}
BlocSubject
+ Verb (action)
+ State
sealed class CounterState {}
final class CounterInitial extends CounterState {}
final class CounterLoadInProgress extends CounterState {}
final class CounterLoadSuccess extends CounterState {}
final class CounterLoadFailure extends CounterState {}
sealed class CounterState {}
final class Initial extends CounterState {}
final class Loading extends CounterState {}
final class Success extends CounterState {}
final class Succeeded extends CounterState {}
final class Loaded extends CounterState {}
final class Failure extends CounterState {}
final class Failed extends CounterState {}
: 비동기 데이터들의 시퀀스
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
yield i;
}
}
// 수형 파라미터인 max값까지 정수들의 Stream을 반환
async*
함수를 이용하면 Stream이 생성 가능함yield
키워드를 사용해서 데이터의 Stream
을 변환할 수 있음async*
함수에서 yield
를 할 때마다, Stream
으로 푸시Stream사용
Future<int> sumStream(Stream<int> stream) async {
int sum = 0;
await for (int value in stream) {
sum += value;
}
return sum;
}
// 스트림의 각 값들을 기다리고 모든 정수들의 합을 반환
void main() async {
/// Initialize a stream of integers 0-9
Stream<int> stream = countStream(10);
/// Compute the sum of the stream of integers
int sum = await sumStream(stream);
/// Print the sum
print(sum); // 45
}
: BlocBase를 확장한 클래스로 어떤 타입의 생태라도 관리할 수 있도록 확장할 수 있다.
Cubit
은 상태의 변화를 트리거하는 함수를 가지고 있다.Cubit
의 출력이며 애플리케이션의 상태의 일부를 나타냄 → UI 구성 요소들은 상태에 대한 정보를 받고, 현재 상태를 기반으로 스스로를 다시 그림예제)
CounterCubit
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
}
// Cubit이 관리할 상태의 타입을 정의
// 위의 CounterCubit의 경우에, 상태는 int로 표현되지만
// 더 복잡한 경우에는 primitive 타입 대신에 class를 사용함
초기값 정의
class CounterCubit extends Cubit<int> {
CounterCubit(int initialState) : super(initialState);
}
// 위 CounterCubit 와 달리 외부에서 initialState 값을 받을 수 있다
인스턴스
final cubitA = CounterCubit(0); // state starts at 0
final cubitB = CounterCubit(10); // state starts at 10
: 각 Cubit
은 emit을 사용해 새로운 상태 값을 만들 수 있음
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
**void increment() => emit(state + 1);**
// 외부에서 CounterCubit에게 상태 값의 증가를 알릴 수 있게 하는 메소드
// increment가 호출될 때, state getter를 이용해 Cubit의 현재 상태에 접근할 수 있고
// 현재 상태에 1을 더함으로써 새로운 상태를 emit 함
}
void main() {
final cubit = CounterCubit();
print(cubit.state); // 0
cubit.increment();
print(cubit.state); // 1
cubit.close(); // 내부의 상태 스트림을 닫기 위해 Cubit에 close를 호출
}
: 실시간 상태 업데이트를 수신할 수 있게 해주는 Stream을 가지고 있음
Future<void> main() async {
final cubit = CounterCubit();
**final subscription = cubit.stream.listen(print);
//**CounterCubit을 subscribe하고 각 상태 변화를 출력함
cubit.increment(); // 새로운 상태를 emit하는 increment 함수를 호출
await Future.delayed(Duration.zero);
await subscription.cancel();
await cubit.close();
}
: Cubit
이 새로운 상태를 emit 할 때, Change
가 발생함. onChange
를 재정의함으로써, Cubit
의 모든 변화를 관찰할 수 있음.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
}
Change
는 Cubit
의 상태 업데이트가 이루어진 후 발생한다.Change
는 currentState
와 nextState
로 구성된다.void main() {
CounterCubit()
..increment()
..close();
}
[출력 결과] Change { currentState: 0, nextState: 1 }
: Changes
를 한 곳에서 접근하는 것이 가능함
→모든 Changes
에 응답하고 싶다면 Bloc Observer
를 구현하면 됨
class SimpleBlocObserver extends BlocObserver {
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
}
// BlocObserver를 상속받고 onChange메소드를 재정의하면 됨
void main() {
Bloc.observer = SimpleBlocObserver();
CounterCubit()
..increment()
..close();
}
Change { currentState: 0, nextState: 1 }
CounterCubit Change { currentState: 0, nextState: 1 }
Cubit 내부의 onChange
재정의가 가장 처음에 호출되고 이어서 BlocObserver
의 onChange
가 호출됩니다.
: 모든 Cubit은 에러 발생을 알려주는 메소드인 addError를 가지고 있음
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() {
addError(Exception('increment error!'), StackTrace.current);
emit(state + 1);
}
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}
Cubit
에 대해 모든 에러를 처리하고 싶다면, Cubit
내부에서 onError
를 재정의하면 됨.class SimpleBlocObserver extends BlocObserver {
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('${bloc.runtimeType} $error $stackTrace');
super.onError(bloc, error, stackTrace);
}
}
Bloc
는 state
변화를 트리거하기 위해 함수가 아닌, events
에 의존하는 고급 클래스Bloc
는 BlocBase
를 확장하고 그것은 Cubit
과 비슷한 public API를 가진다는 것을 의미
Blocs
는function
을 호출하고 새로운state
를 emit 하기 보다,
events
를 수신하고 수신된 events를 출력될 states로 변환
CounterEvent
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
}
상태 변화 - on
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) {
emit(state + 1);
});
}
}
CounterIncrementPressed
이벤트를 관리하기 위해 EventHandler을 등록CounterIncrementPressd
이벤트에서 state getter와 emit(state+1)를 통해 bloc의 현재 상태이 접근이 가능Future<void> main() async {
final bloc = CounterBloc(); // 인스턴스 생성
print(bloc.state); // 0 - 초기상태 출력
bloc.add(CounterIncrementPressed()); // 이벤트 추가
await Future.delayed(Duration.zero);
print(bloc.state); // 1 - 상태 변화
await bloc.close(); // 스트림 종료
}
Future<void> main() async {
final bloc = CounterBloc();
final subscription = bloc.stream.listen(print); // 구독
bloc.add(CounterIncrementPressed()); // 이벤트 추가, 새로운 상태 emit
await Future.delayed(Duration.zero);
await subscription.cancel(); // 구독 수신 닫기
await bloc.close();
}
Cubit
처럼, Bloc
도 Stream
의 특정한 타입이고 이는 Bloc
또한 subscribe하여 상태를 실시간으로 업데이트할 수 있다는 것을 의미sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
**
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}**
}
[출력예시] Change { currentState: 0, nextState: 1 }
현재 상태, 이벤트, 다음상태로 구성
**Transition { currentState: 0, event: Increment, nextState: 1 }**
*onTransition
은 onChange
앞에 호출되며 currentState
에서 nextState
로의 변화를 어떤 이벤트가 트리거 했는지 포함
: 트랜지션을 관찰하고 싶다면 onTransition을 재정의 하면 됨
class SimpleBlocObserver extends BlocObserver {
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('${bloc.runtimeType} $transition');
}
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('${bloc.runtimeType} $error $stackTrace');
super.onError(bloc, error, stackTrace);
}
}