[TIL] 21.04.13

패키지 [bloc] 이어서

Cubit: BlocBase를 상속받은 클래스로, 간단한 상태에 대한 구현을 아주 간결하게 할 수 있도록 도와주는 Bloc 영역의 클래스이다.

Bloc: BlocBase를 상속받은 클래스로, 좀 더 복잡한 상태에 대한 구현을 위한 Bloc 영역의 클래스이다.

지난번에는 Cubit까지 정리했고 Bloc을 확인해보도록 하겠다.

Bloc 기본

앞서 알아봤던 것처럼 Bloc은 Cubit과 동일하게 BlocBase로 시작한다. Bloc이 다른 점은 상태를 변화시키는 방식인데, 아래 그림을 보면 쉽게 이해된다.

Bloc

이전에 Cubit의 경우 상태를 변화시킬 때 Cubit 내에 정의된 메소드에 바로 접근하여 해당 메소드가 emit을 수행, 상태를 변화시킬 수 있었다. Bloc의 경우 그렇게 하지 않고 event를 UI로부터 받아서 Bloc 내부에서 event를 처리, 상태를 변화시킨다.

1
2
3
4
5
enum CounterEvent { increment }

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

이렇게 하면 기본적인 Bloc이 생성된 것이다. Cubit은 int만 정의해주었던 것과 달리, Bloc을 상속받을 때 Event도 함께 정의하도록 한다. 상단의 enum CounterEvent { increment } 는 CounterEvent의 요소 중 하나를 increment로 정의하고 있는 것이다. 여기서 상태를 변화시킬 수 있도록 하려면 아래와 같이 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
}

단순히 increment() 메소드를 내부에 정의해놓았던 Cubit과 달리 Bloc에는 mapEventToState()라는 메소드를 오버라이딩하여 재정의하고 있다.

mapEventToState()라는 이름에서 알 수 있듯 Event를 State로 바꿔준다고 이해할 수 있겠다. 위 코드에서는 switch-case를 통해 event의 종류에 따라 처리를 달리하는데 앞서 enum으로 정의한 CounterEvent.increment의 경우 state + 1을 반환하는 것으로 알 수 있다. yield는 return과 비슷하지만 비동기방식의 return이라고 이해하면 되겠다. 함수가 종료되지 않는 상태로 요청이 올 때마다 연산하는 구문이다.

Bloc을 사용하는 방법은 나중에 실제로 사용되는 예제를 통해 확인하고 넘어가겠다. 간단히 이벤트를 넣는 방법에 대해서만 보자면,

1
2
3
4
5
6
7
8
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();
}

이런식으로 bloc.add(Event)를 통해 처리한다.

Bloc 관찰하기

Cubit에서와 마찬가지로 onChange()를 오버라이딩하여 상태의 변화를 감지할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}

onTransition()이라는 메소드도 있는데, 이것은 상태의 변화가 일어났을 때 어떤 이벤트의 발생으로 상태가 변화했는지 나타낼 수 있다. 이것이 Bloc이 가진 장점 중 하나인 Traceability(추적 가능)인 것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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) {
super.onChange(change);
print(change);
}

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

Cubit 때와 마찬가지로 BlocObserver를 적용시켜 onChange나 onTransition을 통합적으로 탐지하도록 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}
}

추가적인 내용이 몇 가지 있지만 일단 이렇게 넘어가자. 나머지 필요한 내용은 하면서 배우면 된다.

결론

어쨌든 Bloc이나 Cubit이나 BloC 영역에서 동작하는 클래스였다. 이들이 하는 역할은 UI로부터 이벤트를 받아서 상태를 변화시키고, UI에는 상태를 제공하고, 데이터 영역으로부터 데이터를 받아오는 작업을 수행하는 것이다. 어차피 실제 프로젝트에서 구현하는 것은 나중에 할 것이고, 지금은 기본적인 역할 및 개념만 익히면 될 것 같다.

다음번에는 UI 영역에서 BloC 영역을 사용하기 위한 여러 위젯들을 담고 있는 flutter_bloc 패키지를 알아보겠다.

Author

TaeBbong Kwon

Posted on

2021-04-13

Updated on

2023-01-02

Licensed under

Comments