상태 관리(State Management) 1편 - State Ful
상태 관리(State Management) 2편 - Value Listenerable
상태 관리(State Management) 3편 - Get X [Simple]
상태 관리(State Management) 4편 - Get X [Reactive]
상태 관리(State Management) 5편 - Provider
상태 관리(State Management) 7편 - Cubit
상태 관리(State Management) 8편 - Riverpod
상태 관리(State Management) 9편 - Mobx
Top 7 Flutter State Management Libraries In 2022
Most Popular Packages for State Management in Flutter (2023)
이번 글에서는 flutter의 공식 상태관리 라이브러리인 Bloc pattern에 대해서 알아보도록 하겠다.
Bloc은 초보자들이 사용하기 어렵고, bloc 특성상 많은 파일이 생성된다는 단점이 있어서 작은 규모의 프로젝트에서는 사용성이 다소 떨어진다고 알려져 있다.
Bloc 패턴을 이전 개발사에서 사용해 보면서 처음 접해봤지만 bloc 써보면 다른 라이브러리들한테는 손이 잘 안간다.
Bloc은 유지 보수 측면에서도 강력하고 간단한 기능은 cubit으로 사용해도 문제가 없으며, 때로는 provider를 결합해서 사용해도 좋다.
이전 개발사에서는 DDD(Domain Driven Design) Clean Architecture와 Bloc / Singleton을 사용하여 개발을 진행하였기에, 한 번 프로젝트를 설계할 때 많은 시간이 소요되는건 어쩔 수 없다고 생각이 된다.
Bloc을 알기 위해서는 꼭 알아야 하는 개념이 몇 개 있는데, Stream / abstract / equatable의 개념은 알고 사용하여야 이해가 쉽게 될 수 있다.
여기서는 상태 관리 라이브러리들 간의 차이점만을 간단히 다루기에 해당 기능들에 대해서는 설명하지 않도록 한다.
bloc: ^8.1.0
flutter_bloc: ^8.1.1
카운터 앱은 Flutter 프로젝트 최초 생성시 기본으로 있는 카운트 앱을 약간 변형하여 리셋 기능을 추가하고 단순히 카운트 상태를 증가/감소만 하는 것이 아닌 얼마 만큼을 증가/감소 시킬지에 대한 상태를 추가하여 해당 값 만큼 증가/감소하는 기능을 가지게끔 만든 예제이다.
모든 상태관리 예제는 해당 기능을 가진 카운트 앱으로 만들어 볼 것이다.
앞으로 모든 상태관리에 동일한 UI파일을 사용할 거여서 상태관리 편에서 UI 내용은 다른 글과 동일할 것이다.
UI는 가운데 카운트를 보여줄 숫자가 있고 바로 하단 Row위젯안에 더하기, 마이너스 아이콘을 배치해뒀다. 그 아래로 reset 기능을 호출할 버튼을 만들었다.
카운트 기능을 사용하는게 단순히 숫자만 올리고 내리는 것이 아니라 얼만큼을 증가시키고 감소시킬지를 선택할 수 있는 넘버 박스들을 왼쪽 상단에 수직으로 배치하여 구성하였다.
여기서는 간단한 상태 관리만 보여주는 정도의 UI여서 다른 글에서 각각의 상태 관리에 대해서 더 깊숙하고 복잡한 UI 구조를 만들어서 사용해 볼 예정이다.
아래 공유한 Git Repository를 방문하면 소스 코드를 오픈해 뒀습니다 !
Stack countScreenPublicUI({
required BuildContext context,
required int count,
required int selectCount,
required Function() onIncrement,
required Function() onDecrement,
required Function() onReset,
required Function(int) onCount,
}) {
return Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
child: Center(
child: Text(
"$count",
style: const TextStyle(
fontSize: 60, fontWeight: FontWeight.bold),
),
)),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: onIncrement,
child: const Icon(
Icons.add_circle_outline,
size: 40,
),
),
const SizedBox(width: 24),
GestureDetector(
onTap: onDecrement,
child: const Icon(
Icons.remove_circle_outline,
size: 40,
),
)
],
),
const SizedBox(height: 24),
GestureDetector(
onTap: onReset,
child: Container(
width: MediaQuery.of(context).size.width / 3,
height: 48,
decoration: BoxDecoration(
color: const Color.fromRGBO(71, 71, 71, 1),
borderRadius: BorderRadius.circular(12)),
child: const Center(
child: Text(
'Reset',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
),
const SizedBox(height: 40),
],
),
Positioned(
top: 20,
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Padding(
padding: const EdgeInsets.only(left: 20),
child: Column(
children: [
countAppSelectedCountBox(
onTap: onCount, selectNumber: selectCount, number: 1),
countAppSelectedCountBox(
onTap: onCount, selectNumber: selectCount, number: 10),
countAppSelectedCountBox(
onTap: onCount, selectNumber: selectCount, number: 20),
countAppSelectedCountBox(
onTap: onCount, selectNumber: selectCount, number: 50),
countAppSelectedCountBox(
onTap: onCount, selectNumber: selectCount, number: 100),
],
),
),
),
),
],
);
}
GestureDetector countAppSelectedCountBox({
required Function(int) onTap,
required int number,
required int selectNumber,
}) {
return GestureDetector(
onTap: () => onTap(number),
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: selectNumber == number
? const Color.fromRGBO(91, 91, 91, 1)
: const Color.fromRGBO(61, 61, 61, 1),
borderRadius: BorderRadius.circular(12)),
child: Center(
child: Text(
'$number',
style: TextStyle(
fontWeight: FontWeight.bold,
color: selectNumber == number
? Colors.white
: const Color.fromRGBO(155, 155, 155, 1)),
)),
),
),
);
}
Bloc을 사용하기에 앞서 BlocProvider로 사용할 Bloc을 생성해 주어야 한다. 이 부분은 provider와 똑같으면 bloc 또한 MultiBlocProvider를 사용하여 여러 개를 한 번에 생성할 수도 있다.
Bloc을 사용하려면 BlocBuilder를 생성하여 사용할 수 있다. 이 외에도 BlocListener, BlocConsumer 등의 기능도 있지만 해당 기능은 다음에 다룰 예정이다.
Bloc의 함수를 호출하려면 context.read를 사용하여 .add 기능에 event를 넘겨주어 실행시킨다.
BlocProvider<CountAppBlocBloc>(
create: (context) => CountAppBlocBloc(),
child: BlocBuilder<CountAppBlocBloc, CountAppBlocState>(
builder: (context, state) {
return Scaffold(
appBar: appBar(title: 'Count App With BLoC'),
body: countScreenPublicUI(
context: context,
count: state.count,
selectCount: state.selectCount,
onIncrement: () {
HapticFeedback.mediumImpact();
context.read<CountAppBlocBloc>().add(CountAppBlocIncrement());
},
onDecrement: () {
HapticFeedback.mediumImpact();
context.read<CountAppBlocBloc>().add(CountAppBlocDecrement());
},
onReset: () {
HapticFeedback.mediumImpact();
context.read<CountAppBlocBloc>().add(CountAppBlocReset());
},
onCount: (int number) {
HapticFeedback.mediumImpact();
context
.read<CountAppBlocBloc>()
.add(CountAppBlocSelect(number));
},
),
);
},
),
);
Bloc을 만드는 방법에 대해서 알아보도록 하겠다. 이 부분이 많이 헷갈리고 어렵겠지만 여러 예제나 유튜브 영상을 보면서 연습을 계속해야 익숙해진다.
Bloc은 아래와 같이 생성할 수도 있고 추상화 클래스로 생성하는 방법도 있으며, freezed라는 dart annotation을 활용하여 만드는 방법 등 다양한 방법이 있다.
이렇게 여러 개의 다양한 방법과 커스텀 bloc등이 존재하기에 bloc이 처음인 사람은 어렵겠지만 다양한 방법이 있는 것이기에 하나씩 해보면서 이해하면 된다.
Bloc 생성시 공통점은 반드시 3개의 파일의 기능이 필요하다는 것이다. bloc의 상태를 관리하는 state, event를 관리해주는 event, 비즈니스 로직을 담는 bloc 이렇게 3개의 파일을 나눠서 관리하고 생성하여야 한다.
원래 State안에서 보통 로딩/로딩중/로딩완료 등의 상태를 나눠서 생성을 해주는데, 카운트 앱은 간단한 앱이어서 하나의 state만 생성해 주었다.
copyWith()는 딥 카피라는 기능을 위해서 사용하는 것인데, 딥 카피는 개발을 하면서 꼭 알아야하는 기능이다.
count와 selectCount 변수를 여기서 같이 생성해 준다.
해당 state 아래 부분에 있는 리스트 객체의 접근자로 count, selectCount를 넣어준다.
class CountAppBlocState extends Equatable {
final int count;
final int selectCount;
const CountAppBlocState({
this.count = 0,
this.selectCount = 1,
});
CountAppBlocState copyWith({
int? count,
int? selectCount,
}) {
return CountAppBlocState(
count: count ?? this.count,
selectCount: selectCount ?? this.selectCount,
);
}
List<Object> get props => [count, selectCount];
}
Event를 생성해주는 부분이다.
앞서 Get, provider를 알아보면서 가장 익숙한 부분이 event 부분일 것이다.
Bloc은 다른 라이브러와 가장 큰 차이점이 바로 event가 분리되어 있다는 점이다.
abstract class CountAppBlocEvent extends Equatable {
const CountAppBlocEvent();
List<Object> get props => [];
}
class CountAppBlocIncrement extends CountAppBlocEvent {}
class CountAppBlocDecrement extends CountAppBlocEvent {}
class CountAppBlocReset extends CountAppBlocEvent {}
class CountAppBlocSelect extends CountAppBlocEvent {
final int selectCount;
const CountAppBlocSelect(this.selectCount);
List<Object> get props => [selectCount];
}
state, event에서 만들어준 기능으로 bloc안에 로직을 넣어서 상태를 변경시켜주는 부분이다.
Bloc이 생성될 때 super 인자로 위에서 만들어준 state 객체를 초기화하여 생성하고 bloc안에 event를 만들어 준것이라고 이해하면 된다.
해당 event들은 state의 딥 카피를 사용하여 원하는 변수의 상태 값만을 변경해주면 된다.
class CountAppBlocBloc extends Bloc<CountAppBlocEvent, CountAppBlocState> {
CountAppBlocBloc() : super(const CountAppBlocState()) {
on<CountAppBlocIncrement>(_increment);
on<CountAppBlocDecrement>(_decrement);
on<CountAppBlocReset>(_reset);
on<CountAppBlocSelect>(_select);
}
void _increment(
CountAppBlocIncrement event, Emitter<CountAppBlocState> emit) {
emit(state.copyWith(count: state.count + state.selectCount));
}
void _decrement(
CountAppBlocDecrement event, Emitter<CountAppBlocState> emit) {
emit(state.copyWith(count: state.count - state.selectCount));
}
void _reset(CountAppBlocReset event, Emitter<CountAppBlocState> emit) {
emit(state.copyWith(count: 0));
}
void _select(CountAppBlocSelect event, Emitter<CountAppBlocState> emit) {
emit(state.copyWith(selectCount: event.selectCount));
}
}
https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/count_app/bloc
Bloc을 보면 Get, provider 등과 유사한 것 같지만 또 어떻게 보면 많이 다른점도 있다는 것을 알 수 있다.
조금 어려울 수 있겠지만 공유된 Git 저장소에서 소스 코드를 다운받아 값을 변경해 가면서 실행해 보면 이해가 잘 될 것이다.
Bloc 패턴은 한 번에 이해하기 어려운 상태 관리 방식이지만 유지 보수와 디버깅 차원에서 보면 정말 강력한 기능을 제공해주고 있다.
다수의 기업들이 bloc을 사용하고 있고, 규모가 커지면 커질 수록 bloc에 대한 도입을 고민할 수 밖에 없기에 꼭 알아두면 좋다.
Bloc 패턴 또한 다른 라이브러리 처럼 기능이 너무 많고 다뤄볼 내용이 많아서 자세하게 다루는 내용의 글을 따로 작성하도록 할 예정이다.