Dart & Flutter

[Flutter] BLOC: state management

ju_young 2023. 2. 7. 00:38
728x90

Stream

bloc에 대해 알기 전에 우선 stream을 알아야한다.

 

stream을 처음 접한다면 파이프 안에 물이 흐르는 것을 생각하면 된다. 이때 파이프가 Stream이고 물이 비동기 데이터이다.

 

다음과 같이 stream을 사용하여 간단한 함수를 작성할 수 있다.

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

async*yield를 사용하여 지정한 max 파라미터  값만큼 반복해서 stream integer 값이 반환된다. 앞서 비유한 것처럼 마치 물흐르듯이 값이 나오는 것이다.

 

만약 stream 값들의 합을 반환하고 싶다고 한다면 다음과 같이 asyncawait를 사용할 수 있다.

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

 

await는 stream의 모든 값들을 더할때까지 (for문이 끝날 때까지) 기다린다는 의미이고 끝나면 다음 코드가 진행된다.

 

*Future: 값을 반환하면서 바로 결과를 계산하는 동기 연산과 달리 비동기 연산은 실행될때 바로 결과를 얻을 수 없다. 비동기 연산은 어떤 외부 프로그램으로 인해 기다리는 시간이 필요할 것이다. 결과를 사용할 수 있을때까지 모든 연산을 차단하는 대신 비동기 연산은 Future를 바로 반환할 수 있다.

 

💡 Future 이해하기

한 예시 코드를 먼저 확인해보자. 

import "dart:io";
Future<bool> fileContains(String path, String needle) async {
   var haystack = await File(path).readAsString();
   return haystack.contains(needle);
}

void main() {
    Future<bool> isContain = fileContains(path: path, needle: needle);
    
    isContain.then((value){
        print('val: $val')   
     }
    )
    
    print('waiting...')
}

main 함수가 실행되면 다음과 같이 출력이 된다.

waiting...
true

다시 말해 비동기 연산이란 어떤 동작이 완료되지 않아도 다음 동작을 수행하는 것을 말한다. 위 코드에서는 File(path).readAsString() 이라는 동작이 완료되지 않았지만 Future를 바로 반환하여 다음 동작 print('waiting...') 를 수행할 수 있게 하는 것이다. 그리고 await로 동작이 끝날때까지 기다리게한다음 끝나면 haystack.contains(needle)을 수행하여 값을 반환한다.

 

Cubit

 

위 그림을 기억해두고 바로 코드로 넘어가 보자.

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

Cubit을 생성할때 state type을 정의해야하는데 위 코드에서는 Cubit<int> 라고 정의해주었다. 물론 더 복잡한 클래스를 사용할 수도 있다. 그리고 초기 state 값을 setting해주어야하는데 위에서는 super(0) 라고 주었다. 정리하면 state type은 int, 초기 state는 0으로 준 것이다.

 

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

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

increment() 라는 함수를 추가해주었는데 이 함수로 state를 변경되는 것이다. state 값을 변경하기 위해서는 emit이라는 함수를 사용하고 이것은 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);
  }
}

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

// Output
Change { currentState: 0, nextState: 1 }

emit으로 state를 변경하면 Change 가 일어난다고 한다. 그리고 이 Change된 값들은 onChange를 오버라이딩하여 확인할 수 있다.

 

또 다른 방법으로 BlocObserver를 사용할 수 있는데  BlocObserver는 다음과 같이 어떤 bloc이 어떤 Change가 일어났는지를 알 수 있게 해준다.

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

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

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

 

Bloc

bloc은 cubit을 생성하는 것과 유사하지만 state를 정의하는 것 외에 event도 추가로 정의해주어야한다.

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

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

초기 state를 지정해주는 것도 cubit과 비슷하다. 하지만 추가로 event handler를 추가해주어야한다.

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

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

on<Event> API를 통해 on<CounterIncrementPressed>을 등록해주었다. 그리고 CounterIncrementPressed Event가 들어오면 현재 state는 +1 증가하게 된다.

 

bloc은 모든 state 변경을 event handler 내에서 처리하며 bloc, cubit 둘 다 중복 state는 무시한다.

 

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

위처럼 CounterIncrementPressed()를 추가해주면 state가 변경되는 것을 확인할 수 있다. await Future.delayed(Duration.zero)는 event handler가 event를 처리하고 다음 event-loop를 기다리도록 추가한다.

 

bloc의 observer는 onTransition을 오버라이딩하여 현재 state, event, 다음 state를 확인할 수 있다. 현재 state, event, 다음 state로 구성된 것을 Transition이라고 한다.

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

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

// Output
CounterBloc Transition { currentState: 0, event: Increment, nextState: 1 }
CounterBloc Change { currentState: 0, nextState: 1 }

[Reference]

https://dkswnkk.tistory.com/23

https://bloclibrary.dev/#/coreconcepts?id=traceability 

728x90