GetX 에 대해 알아보자

Clean Code Big Poo·2023년 3월 9일
2

Flutter

목록 보기
15/39
post-thumbnail

Overview

지난 포스트에서 상태관리방법에 대해 간략하게 알아보았다. 이번에는 GetX 사용법을 뽀샤보자.
GetX 에 대해 추가로 알게 될 때마다 이곳에 추가할 것!

GetX 설치

GetX Package 에서 Installing 을 참고할 것.

GetX overview

GetX Doc

GetMaterialApp

//main.dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';

import 'app/routes/app_pages.dart';

void main() async {
  await GetStorage.init();//GetStorage에서 후술

  runApp(GetMaterialApp(//instead of "MaterialApp"
    title: 'Moa Cafe',
    initialRoute: AppPages.INITIAL,//아래 "Route설정" 코드 참고
    getPages: AppPages.routes,//아래 "Route설정" 코드 참고
    debugShowCheckedModeBanner: false,
  ));
}

위의 코드는 main.dart 내용이다. MaterialApp대신 GetMaterialApp으로 감싸준 것을 볼 수 있다.
GetMaterialApp은 MaterialApp을 수정한 것이 아니라, MaterialApp을 child로 있는 pre-configured Widget(미리 구성된 위젯)이다.
Route 생성, 주입뿐 아니라, 국제화(localization), 스낵바 등을 지원한다.

예시 코드 상의 AppPages는 아래에서 살펴보자.

Route 설정(GetPage)

//app_routes.dart 

part of 'app_pages.dart';

abstract class Routes {
  Routes._();

  static const HOME = _Paths.HOME;
  static const SPLASH = _Paths.SPLASH;
  static const LOGIN = _Paths.LOGIN;
}

abstract class _Paths {
  _Paths._();

  static const HOME = '/home';
  static const SPLASH = '/splash';
  static const LOGIN = '/login';
}
//app_pages.dart 

import 'package:get/get.dart';

import '../modules/home/home_binding.dart';
import '../modules/home/home_view.dart';
import '../modules/login/login_binding.dart';
import '../modules/login/login_view.dart';
import '../modules/splash/splash_binding.dart';
import '../modules/splash/splash_view.dart';

part 'app_routes.dart';

class AppPages {
  AppPages._();

  static const INITIAL = Routes.SPLASH;

  static final routes = [
    GetPage(
      name: _Paths.HOME,
      page: () => const HomeView(),
      binding: HomeBinding(),
    ),
    GetPage(
      name: _Paths.LOGIN,
      page: () => const LoginView(),
      binding: LoginBinding(),
    ),
    GetPage(
      name: _Paths.SPLASH,
      page: () => const SplashView(),
      binding: SplashBinding(),
    ),
  ];
}

위 코드는 Path 의 정의와 GetPage에 name, page, binding property 에 값을 넣어 화면의 이름과 화면 위젯(View)를 설정한다. 이 이름을 통하여 Get.toName(name) 으로 페이지 이동이 가능하다.

GetView

// splash_view.dart

import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';

import 'splash_controller.dart';

//class SplashView extends StatelessWidget  {//이것도 ok!
class SplashView extends GetView<SplashController> {
  const SplashView({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: GetBuilder<SplashController>(
        init: SplashController(),//이제 하위에서 "controller"로 호출이 가능하다.
        builder: (_) => const Center(
          child: Text('Splash view working...'),
        ),
      ),
    );
  }
}

위 코드에서는 controller를 사용하기 위해 GetView를 extends하고 Controller를 명시하여 주었지만, 그냥 StatelessWidget를 extends 하여도 무방하다. Get.back 정도야 껌이쥐.

GetxController

// splash_controller.dart

import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';

import '../../routes/app_pages.dart';

class SplashController extends GetxController {
  final getStorage = GetStorage();//아래에서 후술
  var id = "1";

  
  void onInit() {
    super.onInit();
  }

  
  void onReady() {
    super.onReady();

    var id = "2";

    //check login in storage
    if (getStorage.read('id') != null) {
      Future.delayed(const Duration(milliseconds: 2000), () {
        Get.offAllNamed(Routes.HOME);
      });
    } else {
      Get.offAllNamed(Routes.LOGIN);
    }
  }

  
  void onClose() {
    super.onClose();
  }
}

GetxController Doc을 통해 자세히 살펴보시길.

LifeCycle

한 객체가 생성되고 동작이 이루어진 뒤, 프로그램내에서 정리되는 과정까지를 말한다. GetX의 LifeCycle은 Obx가 활성화되어야 적상적으로 동작하니 꼭 유의하자.

onInit()

위젯이 메모리에 할당된 직후에 호출된다. 이 옵션을 사용하여 controller에 대한 항목을 초기화할 수 있다.

onReady()

onInit() 다음에 1프레임을 호출한다. 스낵바, 대화상자, 새 Route 또는 비동기 요청과 같은 탐색 이벤트를 입력하기 위한 위치이다.

onClose()

onDelete()이 호출되기 직전에 호출된다. onClose는 컨트롤러에서 사용하는 리소스를 삭제하는 데 사용된다. 이벤트를 닫거나 controller가 파괴되기 전에 Stream을 닫는 로직이 해당된다. 또는 텍스트 편집 컨트롤러, 애니메이션 컨트롤러와 같이 메모리 누수가 발생할 수 있는 개체를 삭제하는 곳이다.

Bindings

//splash_binding.dart

class SplashBinding extends Bindings {
  
  void dependencies() {
    //Get.put(SplashController());

    Get.lazyPut(() => SplashController(), fenix: true);
  }
}

Bindings Doc을 통해 자세히 살펴보시길.

모든 GetPages 와 navigation method는 (ex: Get.to()) binding 속성을 가지고 있다. 이 binding 속성을 route dependencies를 관리한다. put, lazyPut 등을 통해 관리를 하는데, 아래를 살펴보자.

또한 GetMaterialApp을 사용하면 Bindings를 extend 하거나 implement 해주어야 한다.

Get.put

Get.put(controller) 형태로 controller를 instance화 한다.(controller를 메모리에 올린다는 뜻 ㅎ)
controller가 필요한 page마다 controller를 인스턴스화 해줄 수도 있지만, page를 넘어가며 controller를 넘겨 줄 수도 있다.

즉. A Page => B Page 에서 이동하면서 controller를 넘겨 주는 것이다. 다음 코드를 보자.

RaisedButton(
	child: Text('Get put'),
	onPressed: () {
		Get.to(GetPutPage(), binding: BindingsBuilder(() {
		Get.put(DependencyController());
		}));
	},
),

binding 속성을 이용하여 페이지를 전환하면서 사용할 컨트롤러를 보내주는 방법이다.
이동되는 페이지 내에서 Get.put 을 하는 대신, 페이지를 이동하기 전에 Get.put() 을 해주는 것을 볼 수 있다.

Get.lazyPut

Get.lazyPut() 은 Get.put과 달리 페이지가 넘어가도 인스턴스를 곧바로 만들지 않는다. 그럼 언제 생성되느냐? 바로 Get.find()를 호출할 떄이다. (근데 Get.find() 호출하지 않아도 걍 controller 호출함 생성되는 듯?)

즉 내가 원할 때 호출하여 생성할 수 있다.

또한, fenix: true 를 사용하여 이전에 dispose되어도 재구성(재생성)할 수 있다.

Get.putAsync

Get.putAsync()는 리소스를 lazy 하게 로드하고 await한다. storage 나 database를 inject 할 때 사용한다. 즉 controller가 Future를 반환하는 경우에 사용하면 된다. 아래의 GetxService에서 후술한다.

Get으로 하는 이동(Navigator)

Get.to

Get.to(SplashView()) 와 같은 방법으로 페이지를 이동할 수 있다.

Get.toNamed

Get.toNamed(routeName)과 같은 식으로 페이지를 이동할 수 있다. 위의 코드로 예시를 들자면 Get.toNamed(Routes.HOME) 이런식이다.

back 버튼을 이용하여 이전 페이지로 돌아갈 수 있다.

Get.offNamed

Get.offNamed(routeName)과 같은 식으로 페이지를 이동할 수 있다. 위의 코드로 예시를 들자면 Get.offNamed(Routes.HOME) 이런식이다.

back 버튼을 이용하여 2 Step 이전까지 돌아갈 수 있다.

Get.offAllNamed

Get.offAllNamed(routeName)과 같은 식으로 페이지를 이동할 수 있다. 위의 코드로 예시를 들자면 Get.offAllNamed(Routes.HOME) 이런식이다.

이전 페이지로 돌아갈 수 없다.

GetMiddleware

여기를 참고하여 자세히 알아보자.

페이지를 이동할 때 마다 특정 조건을 만족하면 특정 페이지로 이동하는 redirect를 구현 할 때 사용된다.

아래는 로그아웃 상태일때는 로그인 화면으로, 로그인 후에 약관 미동의시에는 약관 동의 화면으로 redirect되는 GetMiddleware 예제 이다.

중요! : 이동하려는 route와 이름이 다른지 꼭 체크해주자. 안그러면 무한루프에 빠진다.

class AuthMiddleware extends GetMiddleware {
  
  RouteSettings redirect(String route) {
    final authService = Get.find<AuthService>();

    if (authService.isLogin) {
      // 로그인 상태

      if (!authService.isAgreedTerm && route != Routes.AGREEMENT_LIST) {
        // 약관미동의시 약관동의 화면으로
        return RouteSettings(name: Routes.AGREEMENT_LIST);
      }
    } else {
      // 로그아웃 상태
      if (!authService.useBioAuth && route != Routes.LOGINVIEW) {
        // 로그인 화면으로
        return RouteSettings(name: Routes.LOGINVIEW);
      }
    }
    Util.print('AuthMiddleware : Page ${route} called');
    return null;//여기에서 return 되면 별도 페이지 이동(redirect)가 없다는 뜻
  }
}

아래와 같이 redirect이 필요한 화면에는 middlewares 파라미터에 AuthMiddleware를 연결한다.

class AppPages {
  AppPages._();

  static const INITIAL = Routes.SPLASH;

  static final routes = [
  	GetPage(name: Routes.SPLASH, page: () => SplashView()),
  ];
  
  GetPage(
        name: _Paths.LOGIN,
        page: () => LoginView(),
        binding: AuthBinding(),
        transition: Transition.zoom,
        middlewares: [AuthMiddleware()]),
        
	// 약관 동의
    GetPage(
        name: _Paths.AGREEMENT,
        page: () => AgreementListView(),
        binding: AgreementBinding(),
        middlewares: [AuthMiddleware()]),//약관동의 화면
    GetPage(
        name: _Paths.AGREEMENT_PERSONAL_INFORMATION,
        page: () => AgreementPersonalInformationView(),
        binding: AgreementBinding(),),//약관동의 디테일 화면
    GetPage(
        name: _Paths.AGREEMENT_LOCATION_SERVICE,
        page: () => AgreementLocationServiceView(),
        binding: AgreementBinding(),),//약관동의 디테일 화면
    GetPage(
        name: _Paths.AGREEMENT_MARKETING,
        page: () => AgreementMarketingView(),
        binding: AgreementBinding(),),//약관동의 디테일 화면      
}

GetStorage

GetStorage은 간단한 Maps 형식의 저장소이다. 하지만 DB는 아니다! 유의할 것.
GetStorage을 사용하기 위해서 추가적인 작업이 필요하다. 아래 링크로 install 하도록 하자.

get_storage Doc
ㅋㅋㅋ자기들 음청 빠르다고 자랑 중

main() async {
  await GetStorage.init();//here
  
  runApp(App());
}

앱을 실행 전에 GetStorage init이 필요하다. Future 타입으로 반환하므로 await와 async를 추가해준다.

위의 작업이 완료되면 아래와 같이 GetStorage를 호출하여 사용할 수 있다.

final box = GetStorage();

만약 또 다른 GetStorage가 필요하면 name을 할당하여 생성할 수 있다.

await GetStorage.init('anotherBox');//like this
final box = GetStorage('anotherBox');

write

box.write('quote', 'GetX is the best');

quote가 key가 되고, GetX is the best가 value가 된다.

Note. Doc을 확인해보면 곧 declare 된다고 한다. await write가 될 거라고..

read

print(box.read('quote'));
// out: GetX is the best

remove

box.remove('quote');

listen

Function? disposeListen;
disposeListen = box.listen((){
  print('box changed');
});

listenKey

box.listenKey('key', (value){
  print('new key is $value');
});

erase

box.erase();

remove와 다른 점은 해당 GetStorage안의 모든 내용이 삭제된다는 것이다.

GetConnect

getX 패턴의 http 호출 방식이다. 예제 말고 개인 프로젝트에서 딥하게 사용해 본적은 없으나 차후에 사용하게 되면 error나 tip을 따로 정리할 예정이다.

설정

//Create a file: dependency_injection.dart

import 'package:get/get.dart';

class DependencyInjection {
 static void init() async {
   Get.put<GetConnect>(GetConnect()); //initializing GetConnect
 }
}

GetConnect 초기화

//main.dart

void main() {
 runApp(const MyApp());
 DependencyInjection.init(); //calling DependencyInjection init method
}

GetConnect를 사용하기 위해 main 에서 init한다.

Rest Api

다음은 GetConnect을 이용하여 Rest API를 사용해보는 예제 이다.

//Create a file: rest_api.dart

import 'package:get/get.dart';

class RestAPI{

 final GetConnect connect = Get.find<GetConnect>();//1. 

 //GET request example
 Future<dynamic> getDataMethod() async {
   Response response = await connect.get('your_get_api_url');//2.
   if(response.statusCode == 200) {
     return response.body;
   }else{
     return null;
   }
 }

 //post request example
 Future<dynamic> postDataMethod() async {

   //body data
   FormData formData = FormData({
     'field_name': 'field_value',
     'field_name': 'field_value',
   });

   Response response = await connect.post('your_post_api_url', formData);//3. 
   if(response.statusCode == 200) {
     return response.body;
   }else{
     return null;
   }
 }

}
  1. controller를 호출할 때 처럼 .find를 사용하여 GetConnect를 호출한다.
  2. Response를 사용할때처럼 url을 날려 Future로 받아온다. 실제로 활용하게 된다면 Future여기에 response model을 넣어주면 되겠다.
  3. 2에서get으로 받아왔으면 이번에는 post로 데이터를 서버로 날리는 작업을 한다. 이후 액션에 대하여는 성공시 (response.statusCode == 200) 조건문에, 실패시 else 조건문에 작성하면 된다.
//dependency_injection.dart

class DependencyInjection {
 static void init() async {

   Get.put<GetConnect>(GetConnect()); 
   Get.put<RestAPI>(RestAPI()); //initializing REST API class 

 }
}

RestAPI 초기화

final RestAPI restAPI = Get.find<RestAPI>();

이게 앱 전체에서 위의 코드를 통해 restAPI접근하여 http 통신을 할수 있게 되었다.

GraphQL API

//Create a file: queries.dart

class GraphqlQuery {
	static String languageListQuery(int limit) => '''
      {
   	   language {
           code
           name
           native
        }
    }
    ''';

static String addLanguageQuery(String code, String name, String native) => '''
      mutation {
 		addLanguageMethod(addLanguage: {
   			code: $code,
   			name: $name,
	            native: $native
 		}){
   			status,
			message
 		}
  }
     }
    ''';

}
//Create a file: graph_api.dart

class GraphAPI {
 final GetConnect connect = Get.find<GetConnect>();

 //GET(query) request example
 Future<dynamic> getLanguages() async {
   Response response;

   try {
     response = await connect.query(
       GraphqlQuery.languageListQuery(10),
       url: "your_api_endpoint",
       headers: {'Authorization': 'Bearer paste_your_jwt_token_key_here'},
     );
     
     final results = response.body['language'];

     if (results != null && results.toString().isBlank == false && response.statusCode == 200) {
       return results;
     } else {
       return null;
     }
   } catch (e) {
     print(e);
   }
 }


 //POST (mutation) request example
 Future<dynamic> addLanguage() async {
   Response response;

   try {
     response = await connect.query(
       GraphqlQuery.addLanguageQuery('HI', 'Hindi', 'India'),
       url: "your_api_endpoint",
       headers: {'Authorization': 'Bearer paste_your_jwt_token_key_here'},
     );
     
     final results = response.body['addLanguageMethod'];

     if (results != null && results.toString().isBlank == false && response.statusCode == 200) {
       return results;
     } else {
       return null;
     }
   } catch (e) {
     print(e);
   }
 }

}
//dependency_injection.dart

class DependencyInjection {
 static void init() async {

   Get.put<GetConnect>(GetConnect()); 
   Get.put<RestAPI>(RestAPI()); 
   Get.put<GraphAPI>(GraphPI()); //initializing Graph API class 

 }
}
final GraphAPI graphAPI = Get.find<GraphAPI>();

GetxService

GetService와 GetController의 가장 큰 차이점은 다음과 같다.

GetxController: 화면이랑 같이 죽는다.
GetxService: 영원히 살아있다.

GetService로 등록한 것은 아래 코드롤 해제해주어야 한다.

Get.reset();

GetX 패턴에서 데이터의 흐름은 이렇게 흐른다:

(백엔드 서버) -> Api -> Repository -> Controller -> View.

그러므로

(백엔드 서버)-> GetConnect -> GetService -> GetController -> GetView

사용자가 화면에서 볼 데이터를 컨트롤러가 관리하는데, 이 컨트롤러에게 데이터를 wrapping해서 넘겨주는 역할을 repository가 한다. 모바일 환경에서 화면 하나는 여러번 생겼다가 사라질 수 있다. 하지만 데이터를 warpping해서 넘겨주는 기능은 앱이 실행되는 내내 살아있어야 한다.

번외

GetStorage VS Sharedpreferences

둘다 로컬 스토리지(local storage)인디... 뭐시중?
결론적으로 말하자면 GetStorage가 더 빠르다(고 한다.)

sharedpreferences는 flutter.dev가 만든거고.. GetStorage는 GetX팀이 만들어낸 것이니..
여러가지 상태관리가 있고 규모가 커질 수록 Get에서 다른 상태관리(Bloc, Provider, riverpad)로 떠나니 둘다 알아 놓는 것이 좋긴 하겠다.

참고

https://absyz.com/getconnect-the-best-way-to-perform-api-operations-in-flutter-with-getx/

https://velog.io/@dc143c_dev/FlutterGetX-%EB%A7%8C%EB%B3%B4%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-3-GetConnect%EB%A1%9C-http-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B3%A0-UI%EC%97%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0

https://velog.io/@broccolism/GetX-Service-vs.-Controller-%EC%B0%A8%EC%9D%B4%EB%A5%BC-GetX-%ED%8C%A8%ED%84%B4%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

0개의 댓글