상태관리 방법 중 많이 쓰이는 GetX에 대해 알아보자.
참조
https://terry1213.github.io/flutter/flutter-getx/
GetX는 매우 가볍고 강력하고, 고성능 상태 관리, 지능형 종속성 주입, 라우트 관리 기능을 제공한다.
dependencies:
get: ^<latest version>
설치
flutter pub get
import
import 'package:get/get.dart';
GetX
를 통하여 할 수 있는 것은 크게 두가지로 나뉜다. 라우트 관리와 상태 관리. 이 두 가지를 분류해서 알아보도록 하자.
Flutter에서 다른 페이지로 이동하거나 Dialog를 띄울 때 같이 라우트간 이동에서 Context를 필요로 한다. 하지만 GetX를 사용하면 Context 없이 라우트를 관리할 수 있다. 따라서 코드가 간결해지고 쉬워진다.
라우트(Route) 관리를 위해서는 GetMaterialApp
을 사용해야 한다.
return GetMaterialApp( //라우트 관리를 하기 위한 GetMaterialApp 선언
title: 'GetX Example',
home: const HomePage(),
getPages: [
GetPage(name: '/next', page: () => const NextPage()),
//Route 사용을 위한 NextPage 이름 설정
],
);
화면 이동 부분을 알아보자.
새로운 화면으로 이동한다. 아래의 코드에서는 NextPage()로 이동한다.
TextButton(
onPressed: () => Get.to(const NextPage()), //해당 페이지로 이동
child: const Text('Get.to()'),
),
미리 설정해둔 이름을 통해 새로운 화면으로 이동한다. 아래의 코드에서는 /next라는 이름을 가진 페이지로 이동한다.
TextButton(
onPressed: () => Get.toNamed('/next'), // 미리 설정해둔 이름을 통해 새로운 화면으로 이동
`child: const Text('Get.toNamed()'),
),
위에서 사용된 /next라는 이름은 위에 GetMaterialApp을 선언할 때 아래처럼 설정해둔 이름이다.
GetPage(name: '/next', page: () => const NextPage()),
이전 화면으로 돌아간다.
TextButton(
onPressed: () => Get.back(), //이전 화면으로 돌아감.
child: const Text('Get.back()'),
),
다음 화면으로 이동하면서 이전 화면을 아에 없애버린다. 이전 화면으로 돌아갈 필요가 없을 때 사용한다.
TextButton(
onPressed: () => Get.off(const NextPage()), // 다음 화면으로 이동하면서 이전 화면을 없애 버린다.
child: const Text('Get.off()'),
),
Get.off()
가 이전 화면 하나만 없앴다면 Get.offAll()
는 이전의 모든 화면을 없애고 다음 화면으로 이동한다.
TextButton(
onPressed: () => Get.offAll(const NextPage()), //off는 전 화면 하나만 없애지만 offAll은 화면 전체를 없앤다.
child: const Text('Get.offAll()'),
),
기본적으로 Snackbar는 하단에서만 나온다. GetX를 사용하면 Snackbar를 상단에도 띄울 수 있다.
제목과 메시지를 설정하면 해당 내용으로 Snackbar를 보여준다. 지속시간(duration)
, 스낵바 위치(snackPosition)
, 배경색(backgroundColor)
등 여러 설정을 추가할 수 있다.
TextButton(
onPressed: () =>
Get.snackbar( //Snackbar 생성
'Snackbar', // Snackbar title,
'Snackbar', // SnackbarDescription,
snackPosition: SnackPosition.TOP), // Snackbar 위치
child: const Text('Get.snackbar()'),
),
Get.snackbar()
와 거의 동일하다. Get.showSnackbar()
는 안에 GetBar()
를 사용한다.
TextButton(
onPressed: () =>
Get.showSnackbar(
GetBar(
title: 'Snackbar', // Snackbar title
message: 'Snackbar', //Snackbar Description
duration: const Duration(seconds: 2), // Snackbar 지속시간
snackPosition: SnackPosition.BOTTOM, // Snackbar 위치
),
),
child: const Text('Get.showSnackbar()'),
),
Dialog를 화면에 띄어준다. 확인/취소 시에 실행할 함수(onConfirm
, onCancel
), 확인/취소 텍스트(textConfirm
, textCancel
), 배경색(backgroundColor
) 등 여러 설정들을 추가할 수 있다.
TextButton(
onPressed: () => Get.defaultDialog( // 기본 대화창 생성
title: 'Dialog', // 대화창 title
middleText: 'Dialog' // 대화창 Description
),
child: const Text('Get.defaultDialog()'),
),
Get.defaultDialog()
와 달리 원래 사용하던 Dialog 위젯을 가져와서 사용할 수 있다. 따라서 새로 시작하는 프로젝트에서 GetX를 적용할 때는 Get.defaultDialog()
를 사용하는 것이 좋지만, 원래 존재하는 프로젝트에 GetX를 적용할 때는 Get.dialog()
를 통해 기존 Dialog 위젯을 복사하여 빠르게 작업하는 편이 좋을 것 같다.
TextButton(
onPressed: () => Get.dialog( // Dialog를 활용해 대화창 생성
const Dialog( // Dialog 위젯 사용
child: SizedBox(
height: 100, // 높이
child: Center(
child: Text('Dialog'),
),
),
),),
child: const Text('Get.dialog()'),
),
내부에 들어갈 위젯만 넣어주면 해당 위젯을 포함하는 BottomSheet를 보여준다.
TextButton(
onPressed: () => Get.bottomSheet( // BottomSheet 사용
Container(
height: 100, // 높이
color: Colors.white, // 배경 색
child: const Center(
child: Text('BottomSheet'),
),
)
),
child: const Text('Get.bottomSheet()'),
),
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GetX Example'),
),
body: Center(
child: Column(
children: [
TextButton(
onPressed: () => Get.to(const NextPage()), //해당 페이지로 이동
child: const Text('Get.to()'),
),
TextButton(
onPressed: () => Get.toNamed('/next'), // 미리 설정해둔 이름을 통해 새로운 화면으로 이동
child: const Text('Get.toNamed()'),
),
TextButton(
onPressed: () => Get.off(const NextPage()), // 다음 화면으로 이동하면서 이전 화면을 아예 없애 버린다.
child: const Text('Get.off()'),
),
TextButton(
onPressed: () => Get.offAll(const NextPage()), //off는 전 화면 하나만 없애지만 offAll은 화면 전체를 없앤다.
child: const Text('Get.offAll()'),
),
TextButton(
onPressed: () =>
Get.snackbar( //Snackbar 생성
'Snackbar', // Snackbar title,
'Snackbar', // Snackbar Description,
snackPosition: SnackPosition.TOP), // Snackbar 위치
child: const Text('Get.snackbar()'),
),
TextButton(
onPressed: () =>
Get.showSnackbar(
GetBar(
title: 'Snackbar', // Snackbar title
message: 'Snackbar', //Snackbar Description
duration: const Duration(seconds: 2), // Snackbar 지속시간
snackPosition: SnackPosition.BOTTOM, // Snackbar 위치
),
),
child: const Text('Get.showSnackbar()'),
),
TextButton(
onPressed: () => Get.defaultDialog( // 기본 대화창 생성
title: 'Dialog', // 대화창 title
middleText: 'Dialog' // 대화창 Description
),
child: const Text('Get.defaultDialog()'),
),
TextButton(
onPressed: () => Get.dialog( // Dialog를 활용해 대화창 생성
const Dialog( // Dialog 위젯 사용
child: SizedBox(
height: 100, // 높이
child: Center(
child: Text('Dialog'),
),
),
),),
child: const Text('Get.dialog()'),
),
TextButton(
onPressed: () => Get.bottomSheet( // BottomSheet 사용
Container(
height: 100, // 높이
color: Colors.white, // 배경 색
child: const Center(
child: Text('BottomSheet'),
),
)
),
child: const Text('Get.bottomSheet()'),
),
],
),
),
);
}
}
class NextPage extends StatelessWidget {
const NextPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('NextPage'),
),
body: Center(
child: Column(
children: [
TextButton(
onPressed: () => Get.back(), //이전 화면으로 돌아감.
child: const Text('Get.back()'),
),
],
),
),
);
}
}
GetX에는 크게 두가지의 상태 관리법이 존재한다. simple
과, reactive
가 있다. count 예제로 두 가지를 나눠서 알아보자.
simple
방식은 reactive
방식보다 메모리를 적게 사용한다.
GetxController
를 extend하는 Controller 클래스를 선언하고, 초기값을 0으로 설정한 count1 변수를 선언한다.
class Controller extends GetxController { // GetxController extend
var count1 = 0; // 변수 선언
}
GetBuilder
를 통해 화면에 count1
변수를 보여준다. 이 때 init
을 설정하지 않으면 에러가 발생한다.
GetBuilder<Controller>(
init: Controller(), // init을 설정하지 않을 시 에러 발생
builder: (_) => Text(
'clicks: ${_.count1}',
),
),
count1 변수를 증가시키고 화면에 알려주는 함수. Provider
의 notifyListeners()
와 동일하다.
void increment1(){
count1++;
update(); // 변수를 증가한 걸 화면에 알려줌. Provider에 notifyListenr()와 동일.
}
reactive는 reactive만의 특별한 기능이 존재한다.
reactive 방식에서는 observable 변수라는 특별한 변수를 사용한다. observable 변수를 Rx라고도 부른다. Rx를 선언하는 방법에는 아래와 같이 3가지가 있다.
1. Value.obs
2. Rx(Value)
3. RxType(Value)
이 중 제일 간단한 1번을 자주 사용한다.
var count2 = 0.obs; // ovservable 변수 선언
Rx
의 값을 접근할 때는 일반적인 변수의 값의 경우와 다르게 .value
를 통해 접근할 수 있다. 여기서 주의해야할 점이 있다. String과 int 같은 primitive type에는 .value
를 사용해야하지만, List에서는 .value
가 필요없다. dart api가 리스트에서만 .value
없이도 값에 접근할 수 있게 해주기 때문이다.
void increment2() => count2.value++; //Rx 값에 접근할 때는 .value 사용
reactive 방식에선 update() 함수가 필요하지 않다.
simple 방식의 GetBuilder과 같은 역할을 하는 것이 GetX이다.
GetX<Controller>( // init을 통해 Controller를 등록할 수 있지만 여기선 Get.put을 사용
builder: (_) => Text(
'clicks: ${_.count2.value}',
),
),
필요한 경우 GetBuilder
에서처럼 init
을 통해 Controller
를 등록할 수 있다.
GetX
보다 더 간단한 방법이 있다. 바로 Obx()
를 사용하는 것이다. Obx()
의 경우 사용할 컨트롤러의 종류를 따로 명시할 필요가 없고, 보여줄 위젯만 리턴하면 된다. 하지만 이 방법은 무조건 Get.put()
을 필요로 한다.
Obx((){ // Obx 사용 시 따로 Controller 명시 X 보여줄 위젯만. 근데 Get.put을 반드시 사용
return Text(
'clicks: ${controller.count2.value}',
);
}),
이전에 말했던 reactive 방식에서만 사용할 수 있는 특별한 기능들이 바로 workers
이다. 이를 사용하면 Rx 변수들의 변화를 감지하고 다양한 상황 별로 적절한 대응을 할 수 있다.
void onInit() {
super.onInit(); // 꼭 호출
once(count2, (_){ // count2가 처음으로 변경 되었을 때만 호출
print('$_이 처음으로 변경되었습니다.');
});
ever(count2, (_){ // count2가 변경될 때마다 호출
print('$_이 변경되었습니다.');
});
debounce( // count2가 변경되다가 마지막 변경 후, 1초간 변경이 없을 때 호출
count2,
(_) {
print('$_가 마지막으로 변경된 이후, 1초간 변경이 없습니다.');
},
time: const Duration(seconds: 1),
);
interval( // count2가 변경되고 있는 동안, 1초마다 호출
count2,
(_) {
print('$_가 변경되는 중입니다.(1초마다 호출)');
},
time: const Duration(seconds: 1),
);
}
Controller에 onInit()
을 override한다. 그 다음 사용하고자 하는 worker
를 등록해주면 된다. 이때 super.onInit()
호출을 잊지 말자.
Get.find()
을 사용하여 increment1()을 호출하는 버튼을 만들어 텍스트 아래에 배치한다.
Get.find<Controller>().increment1, child: Text('increment1'))
하지만 리빌드해보면 Get.find<Controller>()
에서 에러가 발생할 것이다. 이는 Get.find<Controller>()
가 Controller를 찾는 시점이 GetBuilder()
의 init
에서 Controller를 등록하기 이전이라서 그렇다.
이 문제를 해결하기 위해서 Get.put()
을 사용한다.
build()
메소드 내부에서 Get.put()
를 통해 Controller를 등록하여 이를 controller 변수에 할당한다.
build(BuildContext context) {
final controller = Get.put(Controller());
// ...
}
Widget
위의 과정에서 Controller를 등록한 것이기 때문에 GetBuilder
에서 또 등록할 필요가 없다. 따라서 init
부분을 지운다.
GetBuilder<Controller>(
// init 부분 삭제.
builder: (_) => Text(
'clicks: ${_.count1}',
),
)
버튼에서 increment1()를 호출할 때, Get.find() 대신 controller 변수를 사용한다.
TextButton(onPressed: controller.increment1, child: Text('increment1')),
controller.dart
import 'package:get/get.dart';
class Controller extends GetxController { // GetxController extend
var count1 = 0; // 변수 선언
var count2 = 0.obs; // ovservable 변수 선언
void increment1(){
count1++;
update(); // 변수를 증가한 걸 화면에 알려줌. Provider에 notifyListenr()와 동일.
}
void increment2() => count2.value++; //Rx 값에 접근할 때는 .value 사용
void onInit() {
super.onInit(); // 꼭 호출
once(count2, (_){ // count2가 처음으로 변경 되었을 때만 호출
print('$_이 처음으로 변경되었습니다.');
});
ever(count2, (_){ // count2가 변경될 때마다 호출
print('$_이 변경되었습니다.');
});
debounce( // count2가 변경되다가 마지막 변경 후, 1초간 변경이 없을 때 호출
count2,
(_) {
print('$_가 마지막으로 변경된 이후, 1초간 변경이 없습니다.');
},
time: const Duration(seconds: 1),
);
interval( // count2가 변경되고 있는 동안, 1초마다 호출
count2,
(_) {
print('$_가 변경되는 중입니다.(1초마다 호출)');
},
time: const Duration(seconds: 1),
);
}
}
HomePage.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
class HomePage extends StatelessWidget{
const HomePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final controller = Get.put(Controller()); //Get.put을 사용하여 controller 변수에 Controller 할당
return Scaffold(
appBar: AppBar(
title: const Text('Getx example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetBuilder<Controller>(
init: Controller(), // init을 설정하지 않을 시 에러 발생
builder: (_) => Text(
'clicks: ${_.count1}',
),
),
TextButton(
onPressed: controller.increment1,
child: const Text('increment1')),
GetX<Controller>( // init을 통해 Controller를 등록할 수 있지만 여기선 Get.put을 사용
builder: (_) => Text(
'clicks: ${_.count2.value}',
),
),
Obx((){ // Obx 사용 시 따로 Controller 명시 X 보여줄 위젯만. 근데 Get.put을 반드시 사용
return Text(
'clicks: ${controller.count2.value}',
);
}),
TextButton(
onPressed: controller.increment2,
child: const Text('increment2')),
],
),
),
);
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'statemanage/controller.dart';
import 'statemanage/homepage.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp( // 라우터를 사용할 것이 아니기에 MaterialApp 선언
title: 'Getx example',
home: HomePage(),
);
}
}