Flutter 의 많은 아키텍처 패턴들 중 어떤게 대세인지 아직은 잘 모르겠다.
일단은 제일 많이 이야기되는 Bloc 패턴에 대해서 공부해본다.
Bloc 은 Presentation 을 비즈니스 로직과 쉽게 분리하여 코드를 빠르고 쉽게 테스트하고 재사용 할 수 있도록 한다.
- Simple : 이해하기 쉽고 다양한 기술 수준을 가진 개발자들이 사용할 수 있다
- Powerful : 더 작은 컴포넌트들로 구성되어 복잡하고 놀라운,, 어플리케이션을 만들 수 있다
- Testable : 모든걸 쉽게 테스트 가능하게 한다
Cubit
: Bloc 클래스의 기반으로 사용되는 Stream 의 스페셜 타입이다
- Cubit 은 상태 변경을 트리거하는 함수를 노출시킬 수 있다
- 상태는 Cubit 의 아웃풋이고 어플리케이션의 상태의 일부를 나타낸다
- UI 구성요소는 상태 노티를 받고 현재 상태 기반으로 자신의 일부를 다시 그릴 수 있다
1. Creating a Cubit
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
}
- 큐빗 생성1 : 큐빗이 관리 할 상태의 타입 정의가 필요하다
- 위 CounterCubit 의 경우, 상태를 int 타입으로 나타낼 수 있지만 더 복잡한 경우 primitive 타입 대신 class 사용이 필요할 수 있다
- 큐빗 생성2 : 초기 상태를 지정하는게 필요하다
- 초기 상태 값을 가진 super 를 호출하면 된다
- 위 코드를 보면 super(0) 으로 초기 상태를 내부적으로 0 으로 설정하지만 아래 코드 처럼 외부 값을 수용함으로써 큐빗이 더 유연해지도록 할 수 있다
class CounterCubit extends Cubit<int> {
CounterCubit(int initialState) : super(initialState);
}
final cubitA = CounterCubit(0); // state starts at 0
final cubitB = CounterCubit(10); // state starts at 10
2. State Changes
- 각 큐빗은 emit 으로 새로운 상태를 출력할 수 있다
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
- CounterCubit 은 상태를 증가시키는 increment() 메소드가 있고 외부에서 호출할 수 있다
- increment() 가 호출되면 state getter 로 큐빗의 현재 상태에 접근할 수 있고 현재 상테에 1을 더해서 새로운 상태를 emit 할 수 있다
- emit 메소드는 protected 이다
3. Using a Cubit
void main() {
final cubit = CounterCubit();
print(cubit.state); // 0
cubit.increment();
print(cubit.state); // 1
cubit.close();
}
4. Stream Usage
- 큐빗은 Stream 의 스페셜 타입이기 때문에, 큐빗의 상태를 실시간으로 업데이트하기위해 구독할 수 있다
Future<void> main() async {
final cubit = CounterCubit();
final subscription = cubit.listen(print); // 1 : 큐빗 구독 이후의 상태 변화만 수신
cubit.increment();
await Future.delayed(Duration.zero); // 구독을 즉시 취소하지 않기위해
await subscription.cancel(); // 구독 취소 요청
await cubit.close(); // 큐빗 닫기
}
5. Observing a Cubit
- 큐빗이 새로운 상태를 방출할 때 Change 가 생긴다
- onChange() 를 오버라이딩해서 주어진 큐빗에 대한 모든 변화를 observe 할 수 있다
- 큐빗 상태가 업데이트되기 직전에 Change 가 발생한다
- Change 는 currentState, nextState 로 구성되어있다
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
@override
void onChange(Change<int> change) {
print(change);
super.onChange(change);
}
}
void main() {
CounterCubit()
..increment()
..close();
}
// 결과
// Change { currentState: 0, nextState: 1 }
6. BlocObserver
- bloc 라이브러리를 사용하면 한 곳에서 모든 Changes 에 접근할 수 있다는 이점이 있다
- 이 어플리케이션에서는 큐빗이 하나뿐이지만 대규모 어플리케이션에서는 어플리케이션의 상태의 다른 부분들을 관리하는 큐빗이 많이 있고 일반적인 형태다
- 모든 Changes 에 대한 응답으로 무언가를 하려면 BlocObserver 를 만들면 된다
- BlocObserver 에서는 Change 자체 외에도 Cubit 인스턴스에 액세스할 수 있다
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(Cubit cubit, Change change) { // 내부 onChange 가 먼저 출력되고
print('${cubit.runtimeType} $change'); // print 가 출력된다
super.onChange(cubit, change);
}
}
void main() {
Bloc.observer = SimpleBlocObserver();
CounterCubit()
..increment()
..close();
}
// 결과
// Change { currentState: 0, nextState: 1 }
// CounterCubit Change { currentState: 0, nextState: 1 }
7. Error Handling
- 모든 큐빗에는 에러가 발생했을 때 알려주는 addError 메서드가 있다
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() {
addError(Exception('increment error!'), StackTrace.current);
emit(state + 1);
}
@override
void onChange(Change<int> change) {
print(change);
super.onChange(change);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}
- onError 는 BlocObserver 에 override 되어서 모든 에러를 전역으로 처리할 수 있다
- onChange 와 마찬가지로 내부 onError 가 먼저 출력되고 BlocObser override 된 부분이 출력된다
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(Cubit cubit, Change change) {
print('${cubit.runtimeType} $change');
super.onChange(cubit, change);
}
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
print('${cubit.runtimeType} $error $stackTrace');
super.onError(cubit, error, stackTrace);
}
}
Bloc
- Bloc 은 Cubit 의 스페셜 타입으로 들어오는 이벤트를 상태로 변환해서 내보낸다
1. Creating a Bloc
- 관리 할 상태를 정의하는 것 외에 블록이 처리할 수 있는 이벤트도 정의해야 한다
- 그 외 부분은 큐빗과 비슷하다
enum CounterEvent { increment }
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0); // 초기 상태값을 super 로 명시해야한다
}
2. State Changes
- 상태 변경을 트리거하는 기능을 정의하는 CounterCubit 을 직접 사용하는 것과 달리 Bloc 을 사용하는 경우, mapEventToState 를 재정의해야 한다
- mapEventToState 는 들어오는 모든 이벤트를 하나 이상의 나가는 상태로 변환하는 작업을 한다
- 블록은 새로운 상태를 직접 emit 해서는 안된다. 대신 모든 상태 변경은 mapEventToState 내의 들어오는 이벤트에 대응하여 출력되어야 한다
- 블로과 큐빗은 중복된 상태를 무시할 것이다
- 만약 state == nextState 일 때 nextState 를 yield 또는 emit 하면 어떤 상태 변화도 일어나지 않을 것이다
enum CounterEvent { increment }
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.increment:
yield state + 1; // mapEventToState 를 업데이트해서 CounterEvent.increament 를 처리할 수 있다
break;
}
}
}
3. Using a Bloc
Future<void> main() async {
final bloc = CounterBloc();
print(bloc.state); // 0
bloc.add(CounterEvent.increment);
await Future.delayed(Duration.zero);
print(bloc.state); // 1
await bloc.close();
}
- CounterBloc 인스턴스를 만들고 출력하면 초기 상태인 0 이 출력된다
- CounterEvent.increment 타입을 추가한다
- 그럼 mapEventToState 에서 증가되어 1 이 출력된다
4. Observing a Bloc
- onChange 를 사용하여 블럭의 모든 상태 변화를 observe 할 수 있다
enum CounterEvent { increment }
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.increment:
yield state + 1;
break;
}
}
@override
void onChange(Change<int> change) {
print(change);
super.onChange(change);
}
}
void main() {
CounterBloc()
..add(CounterEvent.increment)
..close();
}
// 결과
// Change { currentState: 0, nextState: 1 }
- 블럭은 event-driven 이어서 상태가 어떻게 트리거된건지에 대한 정보를 onTransition 을 오버라이딩해서 알 수 있다
- 어떤 한 상태에서 다른 상태로의 변화를 Transition 이라고 부른다
- Transition 은 현재 상태, 이벤트, 다음 상태로 구성된다
- onTransition 은 onChange 전에 호출되며 currentState 에서 nextState 로의 변경을 트리거한 이벤트를 포함한다
enum CounterEvent { increment }
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.increment:
yield state + 1;
break;
}
}
@override
void onChange(Change<int> change) {
print(change);
super.onChange(change);
}
@override
void onTransition(Transition<CounterEvent, int> transition) {
print(transition);
super.onTransition(transition);
}
}
void main() {
CounterBloc()
..add(CounterEvent.increment)
..close();
}
// 결과
// Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }
// Change { currentState: 0, nextState: 1 }
5. BlocObserver
- 커스텀 BlocObserver 에서 onTransition 을 오버라이드해서 단일 위치에서 발생하는 모든 transition 을 observe 할 수 있다
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(Cubit cubit, Change change) {
print('${cubit.runtimeType} $change');
super.onChange(cubit, change);
}
@override
void onTransition(Bloc bloc, Transition transition) {
print('${bloc.runtimeType} $transition');
super.onTransition(bloc, transition);
}
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
print('${cubit.runtimeType} $error $stackTrace');
super.onError(cubit, error, stackTrace);
}
}
void main() {
Bloc.observer = SimpleBlocObserver();
CounterBloc()
..add(CounterEvent.increment)
..close();
}
// 결과
// Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }
// CounterBloc Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }
// Change { currentState: 0, nextState: 1 }
// CounterBloc Change { currentState: 0, nextState: 1 }
- 블록 인스턴스의 독특한 특징은 블록에 새로운 이벤트가 추가될 때마다 호출되는 이벤트를 재정의할 수 있다는 것이다
- onChange 및 onTransition과 마찬가지로, onEvent 도 글로벌뿐만 아니라 로컬로 재정의할 수 있다
- onEvent 는 이벤트가 추가되는 즉시 호출된다
- 로컬 onEvent 는 BlocObserver 내부 onEvent 전에 호출된다
enum CounterEvent { increment }
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.increment:
yield state + 1;
break;
}
}
@override
void onEvent(CounterEvent event) {
print(event);
super.onEvent(event);
}
@override
void onChange(Change<int> change) {
print(change);
super.onChange(change);
}
@override
void onTransition(Transition<CounterEvent, int> transition) {
print(transition);
super.onTransition(transition);
}
}
class SimpleBlocObserver extends BlocObserver {
@override
void onEvent(Bloc bloc, Object event) {
print('${bloc.runtimeType} $event');
super.onEvent(bloc, event);
}
@override
void onChange(Cubit cubit, Change change) {
print('${cubit.runtimeType} $change');
super.onChange(cubit, change);
}
@override
void onTransition(Bloc bloc, Transition transition) {
print('${bloc.runtimeType} $transition');
super.onTransition(bloc, transition);
}
}
// 결과
// CounterEvent.increment
// CounterBloc CounterEvent.increment
// Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }
// CounterBloc Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }
// Change { currentState: 0, nextState: 1 }
// CounterBloc Change { currentState: 0, nextState: 1 }
6. Error Handling
- 큐빗과 마찬가지로 각 블럭에는 addError 와 onError 메소드가 있다
- 아무데서나 addError 를 호출해서 에러가 발생했을을 알 수 있다
- onError 를 오버라이드하면 모든 에러를 대응할 수 있다
- mapEventToState 내에서 발생하는 예외도 onError 에 보고된다
enum CounterEvent { increment }
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.increment:
addError(Exception('increment error!'), StackTrace.current);
yield state + 1;
break;
}
}
@override
void onChange(Change<int> change) {
print(change);
super.onChange(change);
}
@override
void onTransition(Transition<CounterEvent, int> transition) {
print(transition);
super.onTransition(transition);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}
Cubit vs. Bloc
1. Cubit Advantages : Simplicity
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
- 그러나 블럭은 상태, 이벤트, mapEventToState 를 구현해야 한다
enum CounterEvent { increment }
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.increment:
yield state + 1;
break;
}
}
}
2. Bloc Advantagies
2-1. Traceability
- 상태 변경의 순서와 변경이 생긴 이유를 알 수 있다는 것이다
- 어플리케이션의 기능에 중요한 상태의 경우, 상태 변경과 함께 모든 이벤트를 캐치하기 위해 event-driven 접근 방식을 사용할 수 있어 유용하다
- 예를 들어, 아래처럼 AuthenticationState 를 enum 으로 만들어 관리하는 것이다
- 사용자가 로그 아웃 버튼을 탭하고 애플리케이션에서 로그아웃하도록 요청한다면 사용자의 액세스 토큰이 취소되고 강제로 로그 아웃되었을 수 있다.
- Bloc을 사용하면 이러한 애플리케이션 상태가 어떻게 특정 상태가되었는지 명확하게 추적 할 수 있다.
enum AuthenticationState { unknown, authenticated, unauthenticated }
Transition {
currentState: AuthenticationState.authenticated,
event: LogoutRequested,
nextState: AuthenticationState.unauthenticated
}
- 반면에 큐빗은 위 상황에 대해 아래와 같이 단순하게 정보를 제공한다
Change {
currentState: AuthenticationState.authenticated,
nextState: AuthenticationState.unauthenticated
}
2-2. Advanced ReactiveX Operations
- buffer, debounceTime, throttle 등의 연산자를 활용할 수 있다
- 블럭에 들어오는 이벤트 흐름을 제어하고 변환할 수 있는 event sink 가 있다
- 예를들어 실시간 검색을 구현하는 경우, 속도 제한과 백엔드 비용/부하를 줄이기 위해 요청을 debounce 해야한다
- 아래처럼 구현하면 추가 코드 없이 들어오는 이벤트를 쉽게 debounce 할 수 있다
@override
Stream<Transition<CounterEvent, int>> transformEvents(
Stream<CounterEvent> events,
TransitionFunction<CounterEvent, int> transitionFn,
) {
return super.transformEvents(
events.debounceTime(const Duration(milliseconds: 300)),
transitionFn,
);
}
'기타개발 > Flutter' 카테고리의 다른 글
Streams (0) | 2020.09.19 |
---|---|
[Android vs Flutter] View vs Widget (0) | 2020.09.18 |
Flutter 문서 링크 모음 (0) | 2020.09.13 |
Flutter : StatefulWidget & StatelessWidget (0) | 2020.09.13 |
Dart # 11 : Callable classes, Isolates, Typedefs (0) | 2020.08.28 |
댓글