Coaspe

Flutter - bloc #Core Concepts 본문

Flutter/번역

Flutter - bloc #Core Concepts

Coaspe 2023. 2. 17. 14:02

원문: https://bloclibrary.dev/#/coreconcepts

 

Bloc State Management Library

Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials.

bloclibrary.dev

 

Core Conepts(pacakge:bloc)

package:bloc 을 사용하기 전에 다음 섹션들을 자세하게 읽으세요!

 

bloc package를 사용하는 데 있어서 알아야하는 중요한 코어 컨셉들이 몇가지 있습니다.

다음 섹션들에서, 그 컨셉들을 하나씩 자세하게 살펴보고 counter app에 어떻게 적용되는지 알아봅니다.

 

Streams

Stream이 무엇인지 자세하게 알고 싶다면 Dart Documentation 을 참고하세요.
Stream은 비동기적인 데이터들의 시퀀스 입니다.

 

bloc library를 사용하기 위해서, Streams이 어떻게 작동하는지 이해하는 것이 매우 중요합니다.

 

Streams이 무엇인지 잘 모르겠다면, 물이 흐르는 파이프를 생각해보세요. Stream은 파이프이고, 비동기적 데이터들이 물이 됩니다.

 

Dart에서 async*(async generator) 함수를 사용해서 Stream을 만들 수 있습니다.

Stream<int> countStream(int max) async* {
    for (int i = 0; i < max; i++) {
        yield i;
    }
}

함수를 async*로 마킹하면, yield 키워드를 사용해서 데이터의 Stream을 반환할 수 있습니다. 위의 코드에서, 파라미터인 max의 값까지의 정수들의 Stream을 반환합니다.

 

async* 함수에서 yield를 할 때마다, Stream으로 데이터의 피스를 푸쉬합니다.

 

위의 Stream은 다양한 방법으로 사용이 가능합니다. 만약 정수들의 Stream의 합을 반환하는 함수를 작성한다면, 다음과 같은 코드가 가능합니다:

Future<int> sumStream(Stream<int> stream) async {
    int sum = 0;
    await for (int value in stream) {
        sum += value;
    }
    return sum;
}

위의 함수를 async로 마킹하므로써 await 키워드를 사용할 수 있고, 정수들의 Future 값을 반환합니다. 위의 예제에서, stream의 각 값들을 기다리고 stream의 모든 정수들의 합을 반환합니다.

 

다음과 같이 코드를 정리 할 수 있습니다:

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
}

여기까지 우리는 Streams이 Dart에서 어떻게 작동하는지 알아봤습니다. 이제 bloc package의 코어 컴포넌트인 Cubit에 대해 배워볼까요?


Cubit

Cubit은 BlocBase를 extends한 클래스로 상태를 관리하기 위해 어떤 타입이든 extended 될 수 있습니다.

 

Cubit은 상태의 변화를 트리거하는 함수를 가지고 있습니다.

 

상태는 Cubit의 output이고 우리들의 앱의 상태의 일부를 나타냅니다. UI 컴포넌트들은 상태에 대한 정보를 받고, 현재 상태를 기반으로 스스로를(UI의 부분) 다시 그립(redraw)니다.
Note📄: Cubit의 origin을 알고 싶다면, the following issue을 참고하세요.

Creating a Cubit

CounterCubit을 다음과 같이 구현할 수 있습니다.

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
}

Cubit을 생성 할 때, Cubit이 관리할 상태의 타입을 정의해야합니다. 위의 CounterCubit의 경우에, 상태는 int로 표현되지만 더 복잡한 경우에는 primitive 타입 대신에 class를 사용해야 할 수 있습니다.

 

두번째로 Cubit을 생성할 때 해야하는 것은 초기 상태를 정하는 것입니다. 이 작업은 super에 초기값을 넘겨주며 호출하므로써 이루어집니다. 위의 코드에서, 우리는 내부적으로 초기값을 0으로 세팅했지만 외부의 값을 받을 수 있게 하여 좀 더 유연한 Cubit을 만들었습니다.

class CounterCubit extends Cubit<int> {
  CounterCubit(int initialState) : super(initialState);
}

이것은 CounterCubit 인스턴스를 다음과 같이 다른 초기값들로 초기화할 수 있게 해줍니다.

final cubitA = CounterCubit(0); // state starts at 0
final cubitB = CounterCubit(10); // state starts at 10

State Changes

각 Cubit은 emit을 사용해 새로운 상태 값을 만들 수 있습니다.
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}

위의 코드에서, CounterCubit는 외부에서 CounterCubit에게 상태값의 증가를 알릴 수 있게 하는 increment라는 public 메소드를 가지고 있습니다. increment가 호출될 때, state getter를 이용해 Cubit의 현재 상태에 접근할 수 있고, 현재 상태에 1을 더하므로써 새로운 상태를 emit 합니다.

 

emit 메소드는 protected이고 오직 Cubit 안에서만 사용이 가능합니다.

Using a Cubit

이제 우리는 CounterCubit을 사용할 수 있습니다!

 

Basic Usage

void main() {
  final cubit = CounterCubit();
  print(cubit.state); // 0
  cubit.increment();
  print(cubit.state); // 1
  cubit.close();
}

위의 코드는 CounterCubit 인스턴스를 생성하며 시작합니다. 그 다음 cubit의 초기 값인 현재 값을 출력합니다.(아직 emit이 호출되지 않았기 때문에). 다음으로, 상태를 변화시키기 위해 increment 함수를 트리거합니다. 마지막으로, 0에서 1로 증가한 Cubit의 상태를 다시 출력하고, 내부 상태 stream을 닫기 위해 Cubit에 close를 호출합니다.

 

Stream Usage

 

Cubit은 real-time 상태 업데이트를 수신할 수 있게 해주는 Stream을 가지고 있습니다.

Future<void> main() async {
  final cubit = CounterCubit();
  final subscription = cubit.stream.listen(print); // 1
  cubit.increment();
  await Future.delayed(Duration.zero);
  await subscription.cancel();
  await cubit.close();
}

위의 코드에서, CounterCubit을 subscribing(어떤 값을 수신하는 상태를 유지한다고 생각하면 된다.)하고 각 상태 변화를 출력합니다. 그 다음 새로운 상태를 emit하는 increment 함수를 호출합니다. 마지막으로, 더 이상 업데이트 수신을 원하지 않을때 subscription을 cancel하고 Cubit을 close 합니다.

 

Note📄: 이 예제의 await Future.delayed(Duration.zero)는 subscription을 바로 cancel하는 것을 막기 위해 추가되었습니다. 
Caution: 오직 subsequent 상태 변화만이 Cubit에 대해 listen을 호출 했을 때 수신이 가능합니다.

Observing a Cubit

 

Cubit이 새로운 상태를 emit 할 때, Change가 발생합니다. onChange를 오버라이드하므로써, Cubit의 모든 변화를 관찰할 수 있습니다.

 

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }
}

그 다음 Cubit과 상호작용이 가능하고, 콘솔로 모든 변화의 output을 관찰합니다.

void main() {
  CounterCubit()
    ..increment()
    ..close();
}

위 코드의 결과는 다음과 같습니다:

Change { currentState: 0, nextState: 1 }

 

Note📄: Change는 Cubit의 상태 업데이트가 이루어진 후 발생합니다. Change는 currentState와 nextState로 구성됩니다.

BlocObserver

 

bloc library에서 모든 Changes를 한 곳에서 접근하는 것이 가능합니다. 비록, 위의 코드에서 우리는 하나의 Cubit을 가지지만, 더 큰 앱들은 앱 상태의 다른 부분들을 관리하는 많은 Cubits을 가집니다.

 

만약 모든 Changes에 반응하고 싶다면, 커스텀 BlocObserver을 구현하면 됩니다.

class SimpleBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }
}

 

Note📄: 그저 BlocObserver를 extend하고 onChange 메소드를 재정의 하면 됩니다.

 

위의 SimpleBlocObserver를 사용하고 싶다면, main 함수를 수정하면 됩니다.

void main() {
  Bloc.observer = SimpleBlocObserver();
  CounterCubit()
    ..increment()
    ..close();  
}

위 코드의 결과는 다음과 같습니다:

Change { currentState: 0, nextState: 1 }
CounterCubit Change { currentState: 0, nextState: 1 }

 

Note📄: 내부 onChange 재정의는 처음에 호출되고 이어서 BlocObserver의 onChange가 호출됩니다.
Tip💡: BlocObserver에서 Change 뿐만 아니라 Cubit 인스턴스에 대한 접근도 가능합니다.

Error Handling

Cubit은 에러 발생을 알려주는 메소드인 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) {
    super.onChange(change);
    print(change);
  }

  @override
  void onError(Object error, StackTrace stackTrace) {
    print('$error, $stackTrace');
    super.onError(error, stackTrace);
  }
}
Note📄: 측정 Cubit의 모든 에러를 다루고 싶다면, Cubit 안에 onError를 재정의하세요.

 

전역적으로, 발생하는 모든 에러들을 다루고 싶다면, BlocObserver의 onError를 재정의하세요.

class SimpleBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print('${bloc.runtimeType} $error $stackTrace');
    super.onError(bloc, error, stackTrace);
  }
}

프로그램을 다시 돌리면 다음과 같은 출력을 얻습니다:

Exception: increment error!, #0      CounterCubit.increment (file:///main.dart:21:56)
#1      main (file:///main.dart:41:7)
#2      _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:301:19)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)

CounterCubit Exception: increment error! #0      CounterCubit.increment (file:///main.dart:21:56)
#1      main (file:///main.dart:41:7)
#2      _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:301:19)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)

Change { currentState: 0, nextState: 1 }
CounterCubit Change { currentState: 0, nextState: 1 }

Note📄: onChange에서 그랬던것 처럼, 내부의 onError가 글로벌 BlocObserver 재정의보다 먼저 호출됩니다. 


Bloc

Bloc는 state 변화를 트리거하기 위해 events에 의존하는 함수가 아닌 advanced class 입니다. Bloc는 BlocBase를 extends하고 그것은 Cubit과 비슷한 public API를 가진다는 것을 의미합니다. 그러나, Bloc에서 function을 호출하고 새로운 state를 emitting하기 보다, Blocs는 events를 수신하고 수신된 events를 출력될 states로 변환합니다.

 

Creating a Bloc

Bloc를 생성하는 것은 관리할 상태를 정의하는 것을 제외하면 Cubit을 생성하는 것과 비슷합니다. Bloc가 처리할 이벤트도 반드시 정의해야 합니다.

 

이벤트는 Bloc의 입력입니다. 일반적으로 페이지 로드와 같은 생명주기 이벤트나 버튼 누르기와 같은 사용자 상호 작용에 따라 추가됩니다.
abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);
}

CounterCubit을 정의할 때, super를 사용하여 superclass로 초기 상태 값을 설정해야합니다.

 

State Changes

BlocCubit의 함수와 다르게 on<Event> API를 사용하여 이벤트 핸들러를 등록하도록 합니다. 이벤트 핸들러는 입력으로 들어온 이벤트들을 0 또는 그 이상의 상태로 변환합니다.

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) {
      // handle incoming `CounterIncrementPressed` event
    })
  }
}
Tip💡: EventHandlerEmitter 뿐만 아니라 추가된 이벤트에도 접근이 가능합니다. Emitter는 들어온 이벤트에 반응하여 0 또는 그 이상의 상태들을 emit 할 때 사용됩니다.

 

우리는 CounterIncrementPressed 이벤트를 다루기 위해 EventHandler를 업데이트하면 합니다.

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) {
      emit(state + 1);
    });
  }
}

위의 코드에서, 모든 CounterIncrementPressed을 관리하기 위해서 EventHandler을 등록합니다. 들어오는 모든 CounterIncrementPressed 이벤트에서 우리는 state getter와 emit(state + 1)를 통해 bloc의 현재 상태에 접근이 가능합니다.

 

Note📄: BlocBlocBase를 확장하기 때문에, Cubit 처럼 state getter를 사용하여 언제라도 bloc의 헌재 상태에 접근할 수 있습니다.
Blocs는 절대 직접적으로 새로운 상태를 emit 해서는 안 됩니다. 대신 모든 상태 변경은 EventHandler 내의 수신 이벤트에 대한 응답으로 출력되어야 합니다.
Blocs과 Cubits 모두 중복 상태는 무시합니다. 만약 state == nextState 일 때, State nextState을 emit해도, 아무런 상태 변화가 발생하지 않습니다.

Using a Bloc

이 시점에서, 우리는 CounterBloc의 인스턴스를 생성해서 사용할 수 있습니다!

 

Basic Usage

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();
}

위의 코드는, CounterBloc의 인스턴스를 생성하며 시작합니다. 그 다음 초기 상태인(아직 아무런 emit을 하지 않았기 때문에) Bloc의 현재 상태를 출력합니다. 다음으로, 상태 변화를 트리거하기 위해 CounterIncrementPressed 이벤트를 추가합니다. 마지막으로, 0에서 1로 변한 Bloc의 상태를 출력하고 내부 상태 stream을 close하기 위해 Bloc의 close를 호출합니다.

 

Note📄: await Future.delayed(Duration.zero)은 다음 이벤트 루프 iteration을 기다리는 것을 보장하기 위해 추가되었습니다.(EventHandler가 다음 이벤트를 처리하도록 합니다.)

Stream Usage

 

Cubit 처럼, BlocStream의 특별한 타입이고 그것은 Bloc 상태의 real-time 업데이트를 subscribe 할 수 있다는 것을 의미합니다.

Future<void> main() async {
  final bloc = CounterBloc();
  final subscription = bloc.stream.listen(print); // 1
  bloc.add(CounterIncrementPressed());
  await Future.delayed(Duration.zero);
  await subscription.cancel();
  await bloc.close();
}

위의 코드에서, CounterBloc을 subscribe하고 각 상태 변화에서 print를 호출합니다. 그 다음 on<CounterIncrementPressed> EventHandler 를 트리거하는 CounterIncrementPressed 이벤트를 추가하고, 새로운 상태를 emit 합니다. 마지막으로, 더 이상 업데이트 수신이 필요 없어졌을 때 subscription에 대해 cancel 를 호출하고 Bloc를 close 합니다.

 

Note📄: await Future.delayed(Duration.zero)는 subscription이 바로 cancel되는 것을 피하기 위해 추가되었습니다.

Observing a Bloc

 

BlocBlocBase를 확장하기 때문에, Bloc의 모든 상태 변화는 onChange로 관찰할 수 있습니다.

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }
}

main.dart를 다음과 같이 수정할 수 있습니다:

void main() {
  CounterBloc()
    ..add(CounterIncrementPressed())
    ..close();
}

 위의 코드를 실행하면, 다음과 같은 출력이 나옵니다:

Change { currentState: 0, nextState: 1 }

BlocCubit의 가장 큰 차이점은 Bloc는 event-driven이기 때문에, 무엇이 상태의 변화를 트리거 했는지에 대한 정보를 얻을 수 있습니다.

 

이런 작업은 onTransition을 재정의하므로써 구현할 수 있습니다.

 

한 상태에서 다른 상태로 변하는 것을 Transition이라 부릅니다. Transition은 현재 상태, 이벤트, 그리고 다음 상태로 구성됩니다.
abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }

  @override
  void onTransition(Transition<CounterEvent, int> transition) {
    super.onTransition(transition);
    print(transition);
  }
}

여기에서 다시 main.dart를 실행하면, 다음과 같은 출력을 얻습니다:

Transition { currentState: 0, event: Increment, nextState: 1 }
Change { currentState: 0, nextState: 1 }
Note📄: onTransitiononChange 앞에 호출되고 currentState에서 nextState로의 변화를 어떤 이벤트가 트리거 했는지 포함합니다.

BlocObserver

 

이전에 그랬던것 처럼, 코드의 한 장소에서 모든 transitions을 관찰하고 싶다면 커스텀 BlocObserveronTransition을 재정의하면 됩니다.

class SimpleBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('${bloc.runtimeType} $transition');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print('${bloc.runtimeType} $error $stackTrace');
    super.onError(bloc, error, stackTrace);
  }
}

SimpleBlocObserver를 이전에 했던것 처럼 초기화합니다.

void main() {
  Bloc.observer = SimpleBlocObserver();
  CounterBloc()
    ..add(CounterIncrementPressed())
    ..close();  
}

이제 위의 코드를 실행하면, 다음과 같은 결과가 출력됩니다:

Transition { currentState: 0, event: Increment, nextState: 1 }
CounterBloc Transition { currentState: 0, event: Increment, nextState: 1 }
Change { currentState: 0, nextState: 1 }
CounterBloc Change { currentState: 0, nextState: 1 }
Note📄: local,global 각각의 onTransitiononChange보다 먼저 출력됩니다.

 

Bloc 인스턴스의 또다른 특징은 onEvent를 재정의 할 수 있다는 것입니다. onEventBloc에 새로운 이벤트가 추가될 때마다 호출됩니다. onChangeonTransition 처럼 onEvent도 globally, locally 재정의 될 수 있습니다.

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }

  @override
  void onEvent(CounterEvent event) {
    super.onEvent(event);
    print(event);
  }

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }

  @override
  void onTransition(Transition<CounterEvent, int> transition) {
    super.onTransition(transition);
    print(transition);
  }
}
class SimpleBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    print('${bloc.runtimeType} $event');
  }

  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('${bloc.runtimeType} $transition');
  }
}

이제 main.dart를 실행하면 다음과 같은 결과를 출력합니다:

Increment
CounterBloc Increment
Transition { currentState: 0, event: Increment, nextState: 1 }
CounterBloc Transition { currentState: 0, event: Increment, nextState: 1 }
Change { currentState: 0, nextState: 1 }
CounterBloc Change { currentState: 0, nextState: 1 }
Note📄: onEvent는 이벤트가 추가되자마자 호출됩니다. local onEventBlocObserver의 global onEvent보다 먼저 호출됩니다.

Error Handling

Cubit에서 처럼, 각 BlocaddErroronError 메소드를 가지고 있다. Bloc의 내부 어디에서든 addError를 호출하므로써 에러가 발생했다는 것을 나타낼수 있습니다. Cubit에서 처럼, onError를 재정의 하므로써 모든 에러들에 반응할 수 있습니다.

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) {
      addError(Exception('increment error!'), StackTrace.current);
      emit(state + 1);
    });
  }

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(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);
  }
}

여기에서 main.dart를 실행하면, 다음과 같은 에러가 리포트 됩니다:

Exception: increment error!, #0      new CounterBloc.<anonymous closure> (file:///main.dart:117:58)
#1      Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:427:26)
#2      Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:418:25)
#3      Bloc.on.<anonymous closure> (package:bloc/src/bloc.dart:435:9)
#4      _MapStream._handleData (dart:async/stream_pipe.dart:213:31)
#5      _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
#6      _RootZone.runUnaryGuarded (dart:async/zone.dart:1620:10)
#7      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:341:11)
#8      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#9      _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11)
#10     _WhereStream._handleData (dart:async/stream_pipe.dart:195:12)
#11     _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
#12     _RootZone.runUnaryGuarded (dart:async/zone.dart:1620:10)
#13     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:341:11)
#14     _DelayedData.perform (dart:async/stream_impl.dart:591:14)
#15     _StreamImplEvents.handleNext (dart:async/stream_impl.dart:706:11)
#16     _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:663:7)
#17     _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#18     _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#19     _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:120:13)
#20     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:185:5)

CounterBloc Exception: increment error!, #0      new CounterBloc.<anonymous closure> (file:///main.dart:117:58)
#1      Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:427:26)
#2      Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:418:25)
#3      Bloc.on.<anonymous closure> (package:bloc/src/bloc.dart:435:9)
#4      _MapStream._handleData (dart:async/stream_pipe.dart:213:31)
#5      _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
#6      _RootZone.runUnaryGuarded (dart:async/zone.dart:1620:10)
#7      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:341:11)
#8      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#9      _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11)
#10     _WhereStream._handleData (dart:async/stream_pipe.dart:195:12)
#11     _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
#12     _RootZone.runUnaryGuarded (dart:async/zone.dart:1620:10)
#13     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:341:11)
#14     _DelayedData.perform (dart:async/stream_impl.dart:591:14)
#15     _StreamImplEvents.handleNext (dart:async/stream_impl.dart:706:11)
#16     _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:663:7)
#17     _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#18     _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#19     _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:120:13)
#20     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:185:5)

Transition { currentState: 0, event: Increment, nextState: 1 }
CounterBloc Transition { currentState: 0, event: Increment, nextState: 1 }
Change { currentState: 0, nextState: 1 }
CounterBloc Change { currentState: 0, nextState: 1 }
Note📄: local onError가 먼저 실행된 후 global BlocObserveronError가 실행됩니다.
Note📄: onErroronChangeBlocCubit 인스턴스에서 정확히 같은 방법으로 작동합니다.
EventHandler에서 발생한 핸들되지 않은 예외들도 onError로 리포트됩니다.

Cubit vs Bloc

여기까지 우리는 CubitBloc 클래스의 기본을 배웠습니다. 그렇다면 언제 Cubit을 사용하고, 언제 Bloc을 사용해야하는지 궁금하지 않나요?

 

Cubit Advantages

Simplicity

 

Cubit의 가장 큰 장점은 간단하다는 것입니다. Cubit을 생성할 때, 상태와 상태를 변경하기 위한 함수만 정의하면 됩니다. 하지만, Bloc를 생성할 때는 상태, 이벤트, EventHandler까지 구현해야합니다. 이러한 점이 Cubit이 더 이해하기 쉽고 짧은 코드로 작성이 가능하게 합니다. 

 

두 개의 counter 구현을 살펴봅시다:

 

CounterCubit

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}

CounterBloc

abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }
}

Cubit의 구현이 더 간단합니다. 이벤트를 분리해서 정의하기보다, 함수가 이벤트처럼 행동합니다. 게다가, Cubit을 사용할 때, 상태 변화를 트리거 하고 싶다면, 어디서든 emit을 호출하면 됩니다.

 

Bloc Advantages

Traceability

 

Bloc의 가장 큰 장점중 하나는 상태 변화의 시퀀스 뿐만 아니라 무엇이 변화를 트리거 했는지 정확하게 알 수 있다는 것입니다. 앱의 기능적으로 매우 중요한 상태들은, 상태 변화 외에도 모든 이벤트를 포착하기 위해 보다 이벤트 중심적인 접근 방식을 사용하는 것이 매우 유익할 수 있다.

 

흔한 usecase는 AuthenticationState을 관리하는 것 입니다. 단순함을 위해서, AuthenticationStateenum으로 표현해봅시다:

enum AuthenticationState { unknown, authenticated, unauthenticated }

앱의 상태가 authenticated에서 unauthenticated로 바뀌는 것에는 다양한 이유가 있을 것입니다. 예를 들면, 유저가 로그아웃 버튼을 눌러서 앱의 sign out을 요청하는 것 입니다. 한편, 사용자의 액세스 토큰은 파기되고 강제로 로그아웃 되었을 수 있습니다.

Bloc을 사용할 때, 앱의 상태가 어떤 이유로 설정되었는지 추적할 수 있습니다.

Transition {
  currentState: AuthenticationState.authenticated,
  event: LogoutRequested,
  nextState: AuthenticationState.unauthenticated
}

 위의 Transition은 왜 상태가 변화되었는지에 대한 정보를 줍니다. 만약 AuthenticationState를 관리하기 위해 Cubit을 사용했다면, 로그는 다음과 같습니다:

Change {
  currentState: AuthenticationState.authenticated,
  nextState: AuthenticationState.unauthenticated
}

이 로그는 유저가 로그아웃 했다는 것을 알려주지만, 어떤 것이 디버깅 할 때 중요한지를 설명하지 않고, 앱의 상태가 어떻게 변화했는지 이해하는 데 시간이 소모됩니다.

 

Advanced Event Transformations

 

BlocCubit을 앞서는 또 다른 부분은 buffer, debounceTime, throttle와 같은 반응형 오퍼레이터들을 사용해야하는 순간 입니다.

 

Bloc은 들어오는 이벤트의 흐름을 제어하고 변환할 수 있는 이벤트 싱크가 있습니다.

 

예를 들면, real-time 검색을 구현할 때, rate 제한을 피할 뿐만 아니라 벡앤드의 cost/load를 줄이기 위해 백엔드에 대한 요청을 debounce하고 싶을 수 있습니다.

 

Bloc을 사용하면, EventTransformer를 사용하여 Bloc가 이벤트를 처리하는 방식을 바꿀 수 있습니다.

EventTransformer<T> debounce<T>(Duration duration) {
  return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}

CounterBloc() : super(0) {
  on<Increment>(
    (event, emit) => emit(state + 1),
    /// Apply the custom `EventTransformer` to the `EventHandler`.
    transformer: debounce(const Duration(milliseconds: 300)),
  );
}

위의 코드처럼 작성하면, 약간의 추가 코드로 들어오는 이벤트들을 debounce 할 수 있습니다.

 

Tip💡: event transformer의 opinionated set에 대한 정보는 package:bloc_concurrency을 확인하세요.
Tip💡: 여전히 둘 중에 어떤 것을 사용해야할지 잘 모르겠다면, Cubit으로 시작하고 나중에 필요하다면 Bloc으로 리팩토링, 스케일업 하세요.
Comments