상태 관리(State Management) 1편 - State Ful
상태 관리(State Management) 2편 - Value Listenerable
상태 관리(State Management) 3편 - Get X [Simple]
상태 관리(State Management) 5편 - Provider
상태 관리(State Management) 6편 - Bloc
상태 관리(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)
이전 글에서는 Get X 라이브러리의 Simple 방식의 상태관리에 대해서 알아보았다.
이번에는 다른 방법인 Reactive 방식에 대해서 알아보자.
Simple 방식은 기본적으로 GetBuilder 안에서 GetxController의 update()를 실행시켜 상태 변화 알림을 Builder구조 안에서 수신하여 상태를 변경해주고 있었다.
Reactive 방식에서는 GetBuilder 구조가 필요없이 controller를 put 또는 find로 찾아서 해당하는 상태에 값을 알아서 변경해주는 방식이다. 여기서 상태를 찾게 해주기 위해 상태를 변경하고자 하는 UI를 Obx(()=>widget) 형태로 감싸주면 controller가 알아서 해당되는 상태를 찾아가게 해준다.
controller를 여러 개 등록하여도 Obx를 알아서 찾아주기에 사용성은 좋으나 해당 상태가 많아지거나 하면 디버깅이나 유지 보수 측면에서는 다소 성능이 떨어진다고 생각한다.
개발을 하다보면 stream 형태의 listener를 수신하여 처리하는 경우가 있는데, 이럴 경우에는 무조건 Reactive 방식으로 개발을 하는 것이 좋다. Simple 방식은 update()를 호출하여야 하기에 Stream을 처리하기에는 버벅거림이 많이 발생한다.
get: ^4.6.5
카운터 앱은 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)),
)),
),
),
);
}
위젯에서 Get.put으로 컨트롤러를 사용하기에 앞서 등록을 해주고, Get.find로 해당하는 컨트롤러를 찾아와 변수 값을 만들어준다.
Obx로 바디 부분의 위젯을 감싸 바디 아래에 있는 위젯에서 reactive 방식으로 선언된 변수를 사용하게 해주면 된다.
변수의 값을 사용할 때는 .value 값을 꺼내서 사용하여야 한다.
Get.put(CountAppGet());
CountAppGet _controller = Get.find<CountAppGet>();
return Scaffold(
appBar: appBar(title: 'Count App With Get X(Reactive)'),
body: Obx(
() => countScreenPublicUI(
context: context,
count: _controller.rxCount.value,
selectCount: _controller.rxSelectCount.value,
onIncrement: () {
HapticFeedback.mediumImpact();
_controller.increment();
},
onDecrement: () {
HapticFeedback.mediumImpact();
_controller.decrement();
},
onReset: () {
HapticFeedback.mediumImpact();
_controller.reset();
},
onCount: (int number) {
HapticFeedback.mediumImpact();
_controller.changedCount(number);
},
),
));
Reactive 방식으로 변수를 만들기 위해서는 RxType의 변수를 사용하여야 한다. 그리고 변수를 초기화할 때는 변수의 끝에 .obs로 선언해주면 된다.
변수의 값을 변경할 때는 평소 사용한데로 rxCount 값에 다름 변수 값을 주어도 type 에러가 발생하기 때문에 RxInt의 값을 int형에서 대입해 주어야 하는데 .value를 넣어주면 RxType을 DartType의 변수로 변경할 수 있다.
increment()에서는 rxCount.value의 값을 rxCount.value + rxSelectCount.value 의 값을 더한 값으로 변경을 해주면 된다.
decrement()는 현재 카운트 값에서 선택된 값 만큼 빼주면 되고, reset()은 rxCount를 0으로 만들어 주도록 기능을 만들었다.
changedCount() 기능은 얼만큼을 숫자를 증감할지 선택하는 기능으로 선택할 number값을 필수 인자로 받아서 그 값을 대입해주면 된다.
class CountAppGet extends GetxController {
RxInt rxCount = 0.obs;
RxInt rxSelectCount = 1.obs;
void changedCount(int number) => rxSelectCount.value = number;
void increment() => rxCount.value = rxCount.value + rxSelectCount.value;
void decrement() => rxCount.value = rxCount.value - rxSelectCount.value;
void reset() => rxCount.value = 0;
}
https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/count_app/get
이렇게 Get X의 Simple 방식과 Reactive 방식에 대해서 알아보았다.
확실히 Reactive 방식의 사용이 훨씬 편하고 쉽다고 생각이 들지만 막상 Get X로 개발을 하다보면 Reactive 방식은 잘 사용하지 않은 것 같다.
그래도 Get X를 사용할 때 꼭 필요한 순간이 생길 수 있기에 사용 방법에 대해서는 알고 있어야 한다.
다음 시간에는 개인적으로 가장 좋아하는 상태관리 라이브러리인 provider에 대해서 알아보도록 하겠다.