지난 포스트에서 상태관리방법에 대해 간략하게 알아보았다. 이번에는 GetX 사용법을 뽀샤보자.
GetX 에 대해 추가로 알게 될 때마다 이곳에 추가할 것!
GetX Package 에서 Installing 을 참고할 것.
//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는 아래에서 살펴보자.
//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) 으로 페이지 이동이 가능하다.
// 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 정도야 껌이쥐.
// 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을 통해 자세히 살펴보시길.
한 객체가 생성되고 동작이 이루어진 뒤, 프로그램내에서 정리되는 과정까지를 말한다. GetX의 LifeCycle은 Obx가 활성화되어야 적상적으로 동작하니 꼭 유의하자.
위젯이 메모리에 할당된 직후에 호출된다. 이 옵션을 사용하여 controller에 대한 항목을 초기화할 수 있다.
onInit() 다음에 1프레임을 호출한다. 스낵바, 대화상자, 새 Route 또는 비동기 요청과 같은 탐색 이벤트를 입력하기 위한 위치이다.
onDelete()이 호출되기 직전에 호출된다. onClose는 컨트롤러에서 사용하는 리소스를 삭제하는 데 사용된다. 이벤트를 닫거나 controller가 파괴되기 전에 Stream을 닫는 로직이 해당된다. 또는 텍스트 편집 컨트롤러, 애니메이션 컨트롤러와 같이 메모리 누수가 발생할 수 있는 개체를 삭제하는 곳이다.
//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(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.put과 달리 페이지가 넘어가도 인스턴스를 곧바로 만들지 않는다. 그럼 언제 생성되느냐? 바로 Get.find()를 호출할 떄이다. (근데 Get.find() 호출하지 않아도 걍 controller 호출함 생성되는 듯?)
즉 내가 원할 때 호출하여 생성할 수 있다.
또한, fenix: true 를 사용하여 이전에 dispose되어도 재구성(재생성)할 수 있다.
Get.putAsync()는 리소스를 lazy 하게 로드하고 await한다. storage 나 database를 inject 할 때 사용한다. 즉 controller가 Future를 반환하는 경우에 사용하면 된다. 아래의 GetxService에서 후술한다.
Get.to(SplashView()) 와 같은 방법으로 페이지를 이동할 수 있다.
Get.toNamed(routeName)과 같은 식으로 페이지를 이동할 수 있다. 위의 코드로 예시를 들자면 Get.toNamed(Routes.HOME) 이런식이다.
back 버튼을 이용하여 이전 페이지로 돌아갈 수 있다.
Get.offNamed(routeName)과 같은 식으로 페이지를 이동할 수 있다. 위의 코드로 예시를 들자면 Get.offNamed(Routes.HOME) 이런식이다.
back 버튼을 이용하여 2 Step 이전까지 돌아갈 수 있다.
Get.offAllNamed(routeName)과 같은 식으로 페이지를 이동할 수 있다. 위의 코드로 예시를 들자면 Get.offAllNamed(Routes.HOME) 이런식이다.
이전 페이지로 돌아갈 수 없다.
여기를 참고하여 자세히 알아보자.
페이지를 이동할 때 마다 특정 조건을 만족하면 특정 페이지로 이동하는 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은 간단한 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');
box.write('quote', 'GetX is the best');
quote가 key가 되고, GetX is the best가 value가 된다.
Note. Doc을 확인해보면 곧 declare 된다고 한다. await write가 될 거라고..
print(box.read('quote'));
// out: GetX is the best
box.remove('quote');
Function? disposeListen;
disposeListen = box.listen((){
print('box changed');
});
box.listenKey('key', (value){
print('new key is $value');
});
box.erase();
remove와 다른 점은 해당 GetStorage안의 모든 내용이 삭제된다는 것이다.
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한다.
다음은 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;
}
}
}
//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 통신을 할수 있게 되었다.
//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>();
GetService와 GetController의 가장 큰 차이점은 다음과 같다.
GetxController: 화면이랑 같이 죽는다.
GetxService: 영원히 살아있다.
GetService로 등록한 것은 아래 코드롤 해제해주어야 한다.
Get.reset();
GetX 패턴에서 데이터의 흐름은 이렇게 흐른다:
(백엔드 서버) -> Api -> Repository -> Controller -> View.
그러므로
(백엔드 서버)-> GetConnect -> GetService -> GetController -> GetView
사용자가 화면에서 볼 데이터를 컨트롤러가 관리하는데, 이 컨트롤러에게 데이터를 wrapping해서 넘겨주는 역할을 repository가 한다. 모바일 환경에서 화면 하나는 여러번 생겼다가 사라질 수 있다. 하지만 데이터를 warpping해서 넘겨주는 기능은 앱이 실행되는 내내 살아있어야 한다.
둘다 로컬 스토리지(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/