[Flutter] BloC 패턴 알아보기

kimdocs...📄·2023년 11월 27일
0

flutter

목록 보기
2/30

왜 BloC인가?

: 프레젠테이션과 비즈니스 로직을 쉽게 구분할 수 있어 코드가 빠르고, 테스트하기 쉽고, 재사용이 가능해짐


구조

3개의 레이어로 구분한다.

Presentation

: Bloc State에 따라 유저에게 어떻게 보여줄지를 결정한다.

  • 유저의 input과 어플리케이션의 lifecycle event를 제어한다.
  • bloc layer로부터 온 state에 따라 무엇을 보여줄 지 결정한다.

Business

: Bloc layer의 역할은 Presentation Layer으로 부터 발생한 event를 새로운 State로 바꾼다.

  • application의 state를 만들기 위해 한개 이상의 repository에 의존하여 필요한 data를 얻어낼 수도 있다.
  • bloc layer을 유저 인터페이스(presentation layer)와 data layer 사이의 연결 다리 bloc layer은 user input으로 생성된 event를 받아 presentation layer에게 제공할 새로운 state를 만들기 위해 repository와 소통한다.
  • Bloc - to - Bloc Comminication
    • 모든 bloc은 bloc의 변화에 반응할 수 있게 다른 bloc이 구독할 수 있는 state stream이 있다.
    • close 함수 : 메모리 누수 방지를 위해 close가 가능함
      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();
        }
      }

Data

: 소스로부터 데이터를 찾고 조절함.

  • Data Provider
    • raw data를 제공하는 역할
    • 반드시 포괄적이고 다목적으로 쓰여야한다.
    • 예시 ) CRUD작업을 하기 위한 API 호출
  • Repository
    • data provider를 포괄해서 Bloc Layer와 소통할 수 있다.
    • repository layer는 여러개의 data provider와 상호작용할 수 있음
    • business logic layer에 전달하기 위해 data를 가공하는 역할을함

가장 낮은 Layer로 데이터베이스, 네트워크 요청, 비동기 데이터 소스와 상호작용함


네이밍 컨벤션

Event Conventions

  • Bloc 관점에서는 이미 발생한 것들이기 때문에 과거형으로 작성
  • BlocSubject + Noun (optional) + Verb (event)
    • Good
      sealed class CounterEvent {}
      final class CounterStarted extends CounterEvent {}
      final class CounterIncrementPressed extends CounterEvent {}
      final class CounterDecrementPressed extends CounterEvent {}
      final class CounterIncrementRetried extends CounterEvent {}
    • Bad
      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 {}

State Conventions

  • State는 한 시점의 스냅샷이기 때문에 명사로 표현
  • BlocSubject + Verb (action) + State
    • Good
      sealed class CounterState {}
      final class CounterInitial extends CounterState {}
      final class CounterLoadInProgress extends CounterState {}
      final class CounterLoadSuccess extends CounterState {}
      final class CounterLoadFailure extends CounterState {}
    • Bad
      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 {}

Bloc State Management Library


주요 개념

Streams

: 비동기 데이터들의 시퀀스

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;
}
// 스트림의 각 값들을 기다리고 모든 정수들의 합을 반환
  • async 를 사용함으로써 await를 사용할 수 있음
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
}

Cubit

: 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 사용 법

: 실시간 상태 업데이트를 수신할 수 있게 해주는 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 Observer

: 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 }

Bloc Observer

: 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

  • Bloc는 state 변화를 트리거하기 위해 함수가 아닌, events에 의존하는 고급 클래스
  • Bloc는 BlocBase를 확장하고 그것은 Cubit과 비슷한 public API를 가진다는 것을 의미

    Blocs는 function을 호출하고 새로운 stateemit 하기 보다, 
    events를 수신하고 수신된 events를 출력될 states로 변환

Bloc 생성하기

  • event = Bloc의 입력
    • 예시 : 페이지 로드와 같은 생명주기 이벤트나 버튼 누르기와 같은 사용자 상호 작용에 따라 추가

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의 현재 상태이 접근이 가능
✅ Blocs는 절대 직접적으로 새로운 상태를 **`emit`** 해서는 안 된다. 대신 모든 상태 변경은 **`EventHandler`** 내의 수신 이벤트에 대한 응답으로 출력되B어야함

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(); // 스트림 종료
}

Stream 사용법

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하여 상태를 실시간으로 업데이트할 수 있다는 것을 의미

Bloc 관찰하기

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);
  }**
}
  • onChange로 관찰 가능
[출력예시] Change { currentState: 0, nextState: 1 }

Bloc & Cubit의 차이점

  • Bloc은 이벤트 중심이기 때문에 무엇이 상태의 변화를 트리거 했는 지에 대한 정보를 얻을 수 있음
    • onTransition 정의
      • 현재 상태, 이벤트, 다음상태로 구성

        **Transition { currentState: 0, event: Increment, nextState: 1 }**

        *onTransition은 onChange 앞에 호출되며 currentState에서 nextState로의 변화를 어떤 이벤트가 트리거 했는지 포함

Bloc Observer

: 트랜지션을 관찰하고 싶다면 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);
  }
}

profile
👩‍🌾 GitHub: ezidayzi / 📂 Contact: ezidayzi@gmail.com

0개의 댓글