본문 바로가기
기타개발/Flutter

Bloc Library - package:bloc

by 궝테스트 2020. 9. 19.

bloclibrary.dev/#/

 

bloc library

a predictable state management library

bloclibrary.dev

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

댓글