[Flutter] 상태관리 - GetX

송상민·2022년 10월 10일
3

Flutter

목록 보기
6/13
post-thumbnail

상태관리 방법 중 많이 쓰이는 GetX에 대해 알아보자.

참조
https://terry1213.github.io/flutter/flutter-getx/


GetX 특징

GetX는 매우 가볍고 강력하고, 고성능 상태 관리, 지능형 종속성 주입, 라우트 관리 기능을 제공한다.

기본원칙

  • 성능
    • 성능과 리소스 소비의 최소화에 집중.
    • Streams, ChangeNotifier를 사용하지 않음.
  • 생산성
    • 쉽고 간결한 구문을 사용
    • 사용하지 않는 리소스는 메모리에서 자동으로 제거, 개발자가 메모리에서 컨트롤러를 제거하는 것을 신경쓰지 않아도 됨.
  • 조직화
    • 화면 프레젠테이션 로직, 비즈니스 로직, 종속성 주입, 네비게이션을 완전히 분리할 수 있다.
사용을 위한 준비 pubspec.yaml 패키지 추가
dependencies:
	get: ^<latest version>

설치

flutter pub get

import

import 'package:get/get.dart';

GetX를 통하여 할 수 있는 것은 크게 두가지로 나뉜다. 라우트 관리와 상태 관리. 이 두 가지를 분류해서 알아보도록 하자.

라우트(Route) 관리

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 이름 설정
      ],
    );

화면 이동 부분을 알아보자.

1. Get.to()

새로운 화면으로 이동한다. 아래의 코드에서는 NextPage()로 이동한다.

TextButton(
    onPressed: () => Get.to(const NextPage()), //해당 페이지로 이동
    child: const Text('Get.to()'),
),

2. Get.toNamed()

미리 설정해둔 이름을 통해 새로운 화면으로 이동한다. 아래의 코드에서는 /next라는 이름을 가진 페이지로 이동한다.

TextButton(
	onPressed: () => Get.toNamed('/next'), // 미리 설정해둔 이름을 통해 새로운 화면으로 이동
	`child: const Text('Get.toNamed()'),
),

위에서 사용된 /next라는 이름은 위에 GetMaterialApp을 선언할 때 아래처럼 설정해둔 이름이다.

GetPage(name: '/next', page: () => const NextPage()),

3. Get.back()

이전 화면으로 돌아간다.

TextButton(
	onPressed: () => Get.back(), //이전 화면으로 돌아감.
	child: const Text('Get.back()'),
),

4. Get.off()

다음 화면으로 이동하면서 이전 화면을 아에 없애버린다. 이전 화면으로 돌아갈 필요가 없을 때 사용한다.

TextButton(
	onPressed: () => Get.off(const NextPage()), // 다음 화면으로 이동하면서 이전 화면을 없애 버린다.
	child: const Text('Get.off()'),
),

5. Get.offAll()

Get.off()가 이전 화면 하나만 없앴다면 Get.offAll()는 이전의 모든 화면을 없애고 다음 화면으로 이동한다.

TextButton(
	onPressed: () => Get.offAll(const NextPage()), //off는 전 화면 하나만 없애지만 offAll은 화면 전체를 없앤다.
	child: const Text('Get.offAll()'),
),

Snackbar

기본적으로 Snackbar는 하단에서만 나온다. GetX를 사용하면 Snackbar를 상단에도 띄울 수 있다.

1. Get.snackbar()

제목과 메시지를 설정하면 해당 내용으로 Snackbar를 보여준다. 지속시간(duration), 스낵바 위치(snackPosition), 배경색(backgroundColor) 등 여러 설정을 추가할 수 있다.

TextButton(
	onPressed: () =>
	Get.snackbar( //Snackbar 생성
		'Snackbar', // Snackbar title,
		'Snackbar', // SnackbarDescription,
		snackPosition: SnackPosition.TOP),  // Snackbar 위치
		child: const Text('Get.snackbar()'),
),

2. Get.showSnackbar()

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

1. Get.defaultDialog()

Dialog를 화면에 띄어준다. 확인/취소 시에 실행할 함수(onConfirm, onCancel), 확인/취소 텍스트(textConfirm, textCancel), 배경색(backgroundColor) 등 여러 설정들을 추가할 수 있다.

TextButton(
	onPressed: () => Get.defaultDialog( // 기본 대화창 생성
		title: 'Dialog', // 대화창 title
		middleText: 'Dialog' // 대화창 Description
   		),
	child: const Text('Get.defaultDialog()'),
),

2. Get.dialog()

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

1. Get.bottomSheet()

내부에 들어갈 위젯만 넣어주면 해당 위젯을 포함하는 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()'),
          ),
        ],
      ),
    ),
  );
}
} 

상태(State) 관리

GetX에는 크게 두가지의 상태 관리법이 존재한다. simple과, reactive가 있다. count 예제로 두 가지를 나눠서 알아보자.

Simple 방식

simple 방식은 reactive 방식보다 메모리를 적게 사용한다.

1. 변수 선언

GetxController를 extend하는 Controller 클래스를 선언하고, 초기값을 0으로 설정한 count1 변수를 선언한다.

class Controller extends GetxController { // GetxController extend
  var count1 = 0; // 변수 선언
}

2. GetBuilder()

GetBuilder를 통해 화면에 count1 변수를 보여준다. 이 때 init을 설정하지 않으면 에러가 발생한다.

GetBuilder<Controller>(
		init: Controller(), // init을 설정하지 않을 시 에러 발생
        builder: (_) => Text(
             'clicks: ${_.count1}',
  ),
),

3. update()

count1 변수를 증가시키고 화면에 알려주는 함수. ProvidernotifyListeners()와 동일하다.

void increment1(){
    count1++;
    update(); // 변수를 증가한 걸 화면에 알려줌. Provider에 notifyListenr()와 동일.
  }

reactive 방식

reactive는 reactive만의 특별한 기능이 존재한다.

1. Rx(observable 변수) 선언

reactive 방식에서는 observable 변수라는 특별한 변수를 사용한다. observable 변수를 Rx라고도 부른다. Rx를 선언하는 방법에는 아래와 같이 3가지가 있다.

1. Value.obs
2. Rx(Value)
3. RxType(Value)

이 중 제일 간단한 1번을 자주 사용한다.

var count2 = 0.obs; // ovservable 변수 선언

2. Rx(observable 변수)의 값 접근.value

Rx의 값을 접근할 때는 일반적인 변수의 값의 경우와 다르게 .value를 통해 접근할 수 있다. 여기서 주의해야할 점이 있다. String과 int 같은 primitive type에는 .value를 사용해야하지만, List에서는 .value가 필요없다. dart api가 리스트에서만 .value 없이도 값에 접근할 수 있게 해주기 때문이다.

void increment2() => count2.value++; //Rx 값에 접근할 때는 .value 사용

reactive 방식에선 update() 함수가 필요하지 않다.

3. GetX()

simple 방식의 GetBuilder과 같은 역할을 하는 것이 GetX이다.

GetX<Controller>( // init을 통해 Controller를 등록할 수 있지만 여기선 Get.put을 사용
              builder: (_) => Text(
                'clicks: ${_.count2.value}',
	),
),

필요한 경우 GetBuilder에서처럼 init을 통해 Controller를 등록할 수 있다.

4. Obx()

GetX보다 더 간단한 방법이 있다. 바로 Obx()를 사용하는 것이다. Obx()의 경우 사용할 컨트롤러의 종류를 따로 명시할 필요가 없고, 보여줄 위젯만 리턴하면 된다. 하지만 이 방법은 무조건 Get.put()을 필요로 한다.

Obx((){ // Obx 사용 시 따로 Controller 명시 X 보여줄 위젯만. 근데 Get.put을 반드시 사용
           return Text(
          'clicks: ${controller.count2.value}',
	);
}),

5. Workers

이전에 말했던 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() 호출을 잊지 말자.

둘 다 사용

1. Get.find()

Get.find()을 사용하여 increment1()을 호출하는 버튼을 만들어 텍스트 아래에 배치한다.

Get.find<Controller>().increment1, child: Text('increment1'))

하지만 리빌드해보면 Get.find<Controller>()에서 에러가 발생할 것이다. 이는 Get.find<Controller>()가 Controller를 찾는 시점이 GetBuilder()init에서 Controller를 등록하기 이전이라서 그렇다.

2. Get.put()

이 문제를 해결하기 위해서 Get.put()을 사용한다.

build() 메소드 내부에서 Get.put()를 통해 Controller를 등록하여 이를 controller 변수에 할당한다.


Widget build(BuildContext context) {
  final controller = Get.put(Controller());
  // ...
}

위의 과정에서 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(),
    );
  }
}
profile
실력있는 Flutter 개발자가 되어보자

0개의 댓글