GetX
수많은 Flutter 개발자들이 애용하지만 그만큼 안티도 많은 상태관리 패키지인 GetX에 대해서 다뤄보고자 합니다.
GetX는 수많은 플러터 패키지들이 즐비하는 pub.dev 사이트에서 부동의 1위를 굳건히 지키고 있는 상태관리 패키지입니다.
사실 GetX는 상태관리툴이라기보다는 짬뽕툴이긴 합니다. GetX를 import하면 State Management, Navigation Management, Dependencies Management 이 3가지 모두 가능하기 때문이죠.
우선 이번 글에서는 State Management, GetX의 상태관리 하나만 알아보도록 하겠습니다.
플러터의 대표적인 상태관리 툴이라고 한다면, provider, getx, bloc, rivorpod 등이 생각이 나는데요, 사실 제가 지금도 공부 ing이기 때문에 provider랑 getx만 써봤고, 나머지 bloc이랑 rivorpod에 대해서는 잘 모르기 때문에 각 상태관리툴에 대한 상세한 비교는 어려울 것 같지만 다른 블로그에도 비교는 매우 잘 되어 있기 때문에 다른 블로그를 보시는 것도 좋을 것 같습니다!
근데 글을 시작하기에 앞서 이건 확실히 말할 수 있을 것 같네요. provider보단 getx가 훨씬 편하다..!
당연히 getx hater들도 많고 저도 지식이 충분하지 않아서 getx의 단점도 많다는 거 대충 알고 있지만, 중소 프로젝트를 개발할 때에 있어서는 getx가 매우 유용하다고 개인적으로 생각이 듭니다.
일단 이번 글은 GetX 상태관리의 특징들에 대해서 장점 위주로 설명하겠습니다. 단점은 최근에도 많이 영상이나 블로그글로도 많이 올라오니 그것도 많이 참고해주면 좋을 것 같습니답
GetX의 대표적인 특징
- 기존에 flutter/material에 존재하는 Streams나 ChangeNotifier를 사용하지 않는다는 점입니다.
- 왜 WHY? 그 이유는 GetX는 안드로이드, ios만 다루는 것이 아닌, 웹이나 서버 어플리케이션도 다루고 있기 때문에 반응 시간을 단축시키고, RAM을 보다 더 효율적으로 사용할 필요가 있었다고 하네요. 따라서 GetX는 따로 GetValue, GetStream을 만들었습니다.
- GetValue, GetStream은 더 적은 연산 자원으로 낮은 레이턴시와 높은 퍼포먼스를 보여주는데, 이를 바탕으로 상태관리가 이루어지게 됩니다.
- 모든 이벤트를 위한 클래스를 정의해 줄 필요가 없어졌다는 점에서 복잡도가 대폭 낮아집니다.
- BuildContext context가 필요 없습니다.
- context 없이 controller 만으로 변수나 메소드에 접근하여 사용할 수 있습니다. 따라서, 아무 의미 없이 context를 파라미터로 넘겨줄 필요가 없다는 점에서 매우 매우 코드 짜기가 간편해졌다고 볼 수 있습니다.
(개인적으로 provider를 사용할 때 context 넘겨주는 게 좀 까다로웠기 때문에 이런 점은 희소식이었습니다..) - 반면에, context가 필요 없다는 점이 getx의 단점이라고 하는 사람들도 많습니다. 그 이유는 이렇게 context 없는 것에 익숙해져서 flutter를 사용하다보면 나중에 flutter 내부 구조를 이해하기가 어려울거라는 생각이죠. 그 생각도 맞는 말인거 같기 때문에 양쪽 다 공부해야 될 필요성은 있다고 생각합니다.
- context 없이 controller 만으로 변수나 메소드에 접근하여 사용할 수 있습니다. 따라서, 아무 의미 없이 context를 파라미터로 넘겨줄 필요가 없다는 점에서 매우 매우 코드 짜기가 간편해졌다고 볼 수 있습니다.
- GetxController를 구성할 때 세분화해서 구성할 수 있습니다.
- GetX를 사용하면, 위젯이 중첩되더라도, 오로지 변경된 위젯만 다시 빌드됩니다.
예를 들어서, 한 Obx가 Listview를 보고 있고, 또다른 Obx는 그 Listview 안의 Checkbox를 보고 있다면, Checkbox 값이 변경된다 하더라도, Listview는 가만히 있고, Checkbox만 다시 빌드되는 것이죠.
- GetX를 사용하면, 위젯이 중첩되더라도, 오로지 변경된 위젯만 다시 빌드됩니다.
GetX의 마법(?)
static 클래스는 자동 변경할 권한이 없습니다. 따라서, 위젯은 함수 안에 있는 경우에만 변경 가능합니다.
// GetX 사용시..
StreamBuilder( ... ) ?
initialValue: ... ?
builder: ... ?
// 위의 코드들을 더이상 사용하지 않는다.
Obx()
// Obx 위젯 안에 변화시키고 싶은 변수들을 넣기만 하면 된다.
📌 GetX는 Rx 변수의 변경이 있을 때에만 화면에 업데이트 된다.
반응형 변수 선언하기
변수를 "Observable"하도록 만드는 3가지 방법이 있습니다.
- Rx{Type}
// 초기값을 설정하는 것을 추천하지만, 필수는 아닙니다.
final name = RxString('');
final isLogged = RxBool(false);
final count = RxInt(0);
final balance = RxDouble(0.0);
final items = RxList<String>([]);
final myMap = RxMap<String, int>({});
- Rx와 Dart문법의 Generic을 사용하는 Rx
final name = Rx<String>('');
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);
final balance = Rx<Double>(0.0);
final number = Rx<Num>(0);
final items = Rx<List<String>>([]);
final myMap = Rx<Map<String, int>>({});
// 커스텀 클래스 - 그 어떤 클래스도 가능합니다
final user = Rx<User>();
- 사용할 변수에 .obs만 붙여주기 (많이 선호되는 방법이라고 합니다)
final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;
// 커스텀 클래스 - 그 어떤 클래스도 가능합니다
final user = User().obs;
📌 변수의 맨 뒤에 .obs만 붙이기만 하면 되고, 이게 다이다.
📌 이를 통해 변수를 observable하게 만든 것이고, .value를 이용해서 초기값에 접근할 수 있다.
변수를 실제로 적용시켜보기!
// controller file
final count1 = 0.obs;
final count2 = 0.obs;
int get sum => count1.value + count2.value;
// view file
GetX<Controller>(
builder: (controller) {
print("count 1 rebuild");
return Text("${controller.count1.value}");
},
),
GetX<Controller>(
builder: (controller) {
print("count 2 rebuild");
return Text("${controller.count2.value}");
},
),
GetX<Controller>(
builder: (controller) {
print("sum rebuild");
return Text("${controller.sum}");
},
),
List를 사용할 때도 GetX로 🤤
리스트 내 요소와 마찬가지로 리스트 자체 역시 완전히 observable하게 만들 수 있다.
이렇게 할 경우, 리스트에 요소 추가시, 자동적으로 그 리스트를 사용하는 위젯들이 리빌드 된다.
📌 리스트에는 .value를 이용할 필요가 없다. Dart 자체에서 없이도 사용할 수 있게 해준다. String, int와 같은 타입들은 반드시 .value가 필요하지만, getter, setter를 사용하면 이 문제를 해결할 수 있다.
// On the controller
final String title = 'User Info:'.obs
final list = List<User>().obs;
// on the view
Text(controller.title.value), // String은 .value가 필요합니다
ListView.builder (
itemCount: controller.list.length // 리스트는 .value가 필요없습니다.
)
- 자신의 observable 클래스를 업데이트하는 또다른 방법
// model 파일에서
// 각 field들을 observable로 만드는 대신, 클래스 전체를 observable로 만들 것입니다.
class User() {
User({this.name = '', this.age = 0});
String name;
int age;
}
// controller 파일에서
final user = User().obs;
// when you need to update the user variable:
// user의 변수를 업데이트해야할 때
user.update( (user) { // 이 parameter는 업데이트 하길 원하는 인스턴스 자체입니다.
user.name = 'Jonny';
user.age = 18;
});
// user 인스턴스를 업데이트하는 또다른 방법
user(User(name: 'João', age: 35));
// on view:
Obx(()=> Text("Name ${user.value.name}: Age: ${user.value.age}"))
// .value 없이 model의 value에 접근할 수 있습니다.
user().name; // User 클래스가 아니라, user 변수임을 주의하세요 (변수는 소문자 u를 갖고 있습니다.)
Obx()
바인딩을 이용해서 Getx를 입력하는 것은 불필요한 행동입니다. Obx 위젯을 이용해서 변수를 사용하기 위한 controller, Get.find().value 혹은 controller.to.value를 사용해서 value값에 접근할 수 있습니다.
Workers
이벤트가 발생했을 때, 특정 콜백함수 호출을 도와주는 요소라고 볼 수 있습니다.
/// 'count1'이 변경될 때마다 호출
ever(count1, (_) => print("$_ has been changed"));
/// 'count1'이 처음으로 변경될 때 호출
once(count1, (_) => print("$_ was changed once"));
/// Anti DDos - 'count1'이 변경되고 1초간 변화가 없을 때 호출
debounce(count1, (_) => print("debouce$_"), time: Duration(seconds: 1));
/// 'count1'이 변경되고 있는 동안 1초 간격으로 호출
interval(count1, (_) => print("interval $_"), time: Duration(seconds: 1));
⚠️ Worker는 Controller 혹은 클래스를 시작할 때에만 사용할 수 있다. 따라서, 항상 onInit 내에서 사용하거나 (권장함), 클래스 생성자, Stateful Widget initState 내에서(권장은 안하지만 부작용은 없음) 사용해야 한다.
Obx() 좋은 건 알겠고, 그럼 GetBuilder는..?
제가 보기엔 Obx()가 GetBuilder보다 더 편리하고, 코드 길이도 줄일 수 있다는 점에서 더 매력적인 것 같습니다.
그럼에도 GetBuilder가 여전히 Getx를 사용할 때 Obx와 함께 혼용되는 이유는 각자가 상황에 맞게 사용하는 컨셉이 있기 때문입니다.
GetBuilder는 여러가지 상태 관리를 정확하게 하는 것을 목표로 합니다.
GetBuilder가 유용하게 사용되는 경우 예시를 하나 들어보겠습니다.
- 장바구니에 30개의 상품이 들어가있고, 사용자가 삭제를 위해서 각 상품의 삭제 버튼을 클릭하면 상품 목록이 바로 업데이트 되며 가격 및 상품수가 줄어드는 경우를 생각해봅시다.
이러한 경우에 GetBuilder를 사용하는 이유는 상태를 그룹화해서 불필요한 추가적인 연산로직 없이 한 번에 변경해주기 때문입니다.
📌 "개별" 위젯이 많다 -> Obx(), 상태 변화가 여러 번 발생할 것 같다 -> GetBuilder
GetX 상태관리 장점
- 변화가 있는 위젯만 업데이트시킵니다.
- 많은 메모리가 요구되는 ChangeNotifier를 사용하지 않고, 상당히 적은 메모리만을 활용해서 상태 관리 합니다.
- 더 이상 statefulWidget을 사용하지 않아도 됩니다.
-> 모든 위젯을 statelessWidget으로 만들고, 하나의 위젯만 실시간 업데이트가 필요한 경우라면, GetBuilder로 그 위젯을 감싸서 해당 상태를 가져올 수 있습니다. - 프로젝트 정돈이 깔끔해집니다. Controller들을 UI 내에 둘 필요 없이, 내가 만든 Controller 클래스 내에 두면 됩니다.
- 렌더링 직 후, 업데이트를 위해 따로 이벤트를 발생시켜줄 필요가 없어집니다. Controller로부터 직접적으로 이벤트의 호출이 가능해졌기 때문이죠.
- Getx는 전체적으로 대부분의 경우에 어느 타이밍에 메모리를 제거해야 할지를 알고 있습니다. 그래서, 웬만하면 더 이상 사용하지 않는 메모리는 스스로 제거해줍니다. 따라서, 언제 controller를 dispose해야할지만 고려해서 사용하면 됩니다.
GetX 간단 사용 예시 (counter)
// GetxController를 상속(extends)하는 controller 클래스 생성
class Controller extends GetxController {
int counter = 0;
void increment() {
counter++;
update();
// increment()가 호출되었을 때, counter 변수가 변경되어 UI에 반영되어야 한다는 것을 update()로 알려줘야 한다.
}
}
// 내가 만든 Stateless/Stateful 클래스에서,
// increment()가 호출되었을 때 GetBuilder를 이용해 Text를 업데이트
GetBuilder<Controller>(
init: Controller(), // 맨 처음만! 초기화(init)하자
builder: (_) => Text(
'${_.counter}',
),
)
// controller는 처음만 초기화하면 된다. 같은 controller로 GetBuilder를 또 사용하려는 경우에는 init을 하지 말자.
// 중복으로 'init'이 있는 위젯이 배치되자마자, controller는 자동적으로 메모리에서 제거된다.
// Get은 자동적으로 controller를 찾아준다. 그냥 2번 init하지 않으면 된다.
📌 큰 규모의 프로젝트를 진행한다면?
뭐 아직 학생따리인 내가 큰 규모의 프로젝트를 당연히 경험해보지 않았지만, 만약에 큰 규모의 프로젝트를 진행하게 된다면 init을 사용하지 않을 수 있다고 합니다. 이런 경우, binding 클래스를 extends한 클래스를 생성하고, 그 클래스 내에서 해당 route에 생성되어야 하는 controller를 선언하면 됩니다.
// main_binding.dart
// 사용하는 controller들을 Bindings를 extends한 클래스에서 bind하고..
class MainBinding extends Bindings {
@override
void dependencies() {
Get.put<QueryController>(QueryController(), permanent: true);
Get.put<AuthController>(AuthController(), permanent: true);
Get.put<UserController>(UserController(), permanent: true);
Get.put<AmplitudeController>(AmplitudeController(), permanent: true);
}
}
// app.dart
// 이런 식으로 initialBinding을 설정해주는 느낌인 것 같네요.
...
GetMaterialApp(
...
initialBinding: MainBinding(),
initialRoute: AppPages.initial,
getPages: AppPages.routes,
...
),
...
근데, 큰 규모의 플러터 프로젝트에서는 대부분 bloc 상태관리툴을 사용한다고 하는데, 다들 그렇게 사용하는 데에는 이유가 있겠죠? bloc에 대해선 잘 모르지만, getx로 큰 규모의 프로젝트를 진행하기엔 살짝 복잡해질 수도 있을 것 같다는 생각을 해봅니다.
다양한 Route를 사용하면서 예전에 사용한 controller의 데이터가 필요하다면?
- GetBuilder 사용하기 (init X)
class OtherClass extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GetBuilder<Controller>(
builder: (s) => Text('${s.counter}'),
),
),
);
}
GetBuilder 이외의 방법으로 controller 데이터를 불러와야 한다면 Get.find 사용하기
- 아니면 controller 클래스 안의 getter
class Controller extends GetxController {
/// You do not need that. I recommend using it just for ease of syntax.
/// with static method: Controller.to.increment();
/// with no static method: Get.find<Controller>().increment();
/// There is no difference in performance, nor any side effect of using either syntax.
/// Only one does not need the type, and the other the IDE will autocomplete it.
static Controller get to => Get.find(); // add this line
int counter = 0;
void increment() {
counter++;
update();
}
}
실제 코드에선 아래처럼 controller에 접근해서 사용 가능합니다.
FloatingActionButton(
onPressed: () {
Controller.to.increment(),
} // This is incredibly simple!
child: Text("${Controller.to.counter}"),
),
다시 한번 말하지만 statefulWidget은 사용할 필요가 없어요
StatefulWidget 패키지는 Stateless보다 더 많은 RAM 할당이 필요한 큰 크기의 클래스입니다. 100개 이상의 클래스부터는 차이가 있고, 따라서 TickerProviderStateMixin 같은 mixin을 사용해야 하는 경우가 아니면, 굳이 Get을 사용하면서 StatefulWidget은 필요가 없습니다.
StatefulWidget에서 메소드 호출하는 것처럼, GetBuilder를 사용해서 메소드를 호출할 수 있습니다.
initState()나 dispose() 호출할 필요가 있을 때에도, StatefulWidget을 사용할 필요가 없습니다. Get을 사용해서 직접적으로 호출해줄 수 있습니다.
- GetBuilder에서 호출하기
GetBuilder<Controller>(
initState: (_) => Controller.to.fetchApi(),
dispose: (_) => Controller.to.closeStreams(),
builder: (s) => Text('${s.username}'),
),
- GetxController에서 호출하기
@override
void onInit() {
fetchApi();
super.onInit();
}
📌 controller가 처음 불려졌을 때 어떤 메소드가 호출되길 원하는 경우, (좋은 성능을 목표로하는 Get 패키지를 이용하면)이를 위해서 constructor를 사용할 필요가 없습니다. constructor를 사용한다는 것은 controller가 생성되거나 할당되었을 때의 로직에서 벗어나는 일이기 때문에 좋지 않습니다.
📌 onInit()과 onClose()는 이를 위해 만들어졌습니다. Get.lazyPut하는지 여부에 따라, controller가 생성되거나 처음 사용될 때 onInit()과 onClose()가 호출됩니다. API를 호출하기 위한 데이터를 초기화 등을 위해 구식 방식의 initState/dispose를 사용하는 대신 onInit()을 사용하고, stream을 닫는 등의 동작이 필요하면 onClose()를 사용하세요.
출처
https://github.com/jonataslaw/getx/blob/master/README.ko-kr.md
'🐦 플러터' 카테고리의 다른 글
[Flutter] exception in phase 'semantic analysis' in source unit 'BuildScript' Unsupported class file major version 64 (0) | 2023.09.13 |
---|---|
[Flutter] GetX - get.put()과 get.find()의 차이에 대해 알아보자 (0) | 2023.07.13 |
[Flutter] Chewie 패키지를 사용해서 비디오 플레이어를 활용해보자 (0) | 2023.06.22 |
[Flutter] Just_audio 패키지를 사용해서 음악을 삽입해보자 (2) | 2023.06.21 |
[Flutter] Carousel Slider 패키지를 사용해보자 (0) | 2023.06.21 |