[Flutter] GetX 패턴

Taeho-Park·2023년 5월 8일
0
post-thumbnail

들어가기에 앞서..

최근 플러터가 3.0을 지나 멀티플랫폼으로써 큰 부분을 차지하고 있다.
아직도 일부 네이티브 코드를 건드려야 하고 리액트가 건제하다지만
최근 들어 플러터 정말 많이 쓰는 것 같다.

플러터가 공개되고 생태계가 잡힐 때까지 정말 5년정도 걸린 것 같다..

처음 플러터를 사용할 때 디자인 패턴을 어떤 것이 좋을지 고민을 했었는데
이번에 GetX 패턴을 재미삼아 해보았다. 실제로도 유용하다고 판단되어 소개한다.

디자인 패턴은 각자 회사에서 쓰는 방식이 있을 것이다.
또한 소개할 GetX패턴은 포르루갈 출신 개발자 kauemurakami가 제시한 자주 업데이트 되는 패턴이고
무엇보다도 GetX 라이브러리 전용 패턴이다..

즉, GetX를 사용하지 않거나 전혀 관심이 없다면 읽을 필요가 없다.

본인은 GetX를 좋아하고 안정적이라 생각하기 때문에 해당 패턴을 사용하지만
아마 현장에선 사용할 일이 없다고 생각한다.
현업에서 GetX를 제품에 쓰기에는 좀 꺼리는 부분이 있으니..

재미로 봐주시길

GetX 패턴이 뭔데

Git: https://github.com/kauemurakami/getx_pattern
Blog: https://kauemurakami.medium.com/um-pouco-sobre-o-getx-pattern-efb191187d7
Web: https://kauemurakami.github.io/getx_pattern/

위에서 언급한 kauemurakami 개발자가 고안한 패턴이다.
플러터의 상태관리 라이브러리인 GetX를 기반으로 앱을 만들 때 유용하게 관리할 수 있도록 고안되었다. 즉, GetX로 프로젝트를 표준화할 수 있도록 고안한 것이다.

MVC 패턴과 비슷하기도 하고 고안된 목적 또한 다른 패턴과 동일하다.
패턴의 목표는 다음과 같다.

  1. GetX를 사용한 직관적인 프로젝트 개발을 목표로 한다.
  2. 개발 시 겪는 특정 문제를 쉽게 해결한다.
  3. 유지 보수가 쉬우며 누가 빌드하든 달라지지 않는다.
  4. 플러터의 위젯 재사용을 편하도록 한다.
  5. GetX를 전문 프로젝트에 적용할 수 있도록 한다.

이제 그럼 큰 그림부터 살펴보자.


사진이 길어 잘 안보이겠지만 구조는 명확하다.

main.dart 가 있으며
유틸, 확장, 언어, 상수, 테마 따위를 정의하는 core 폴더
앱의 화면을 구성하는 app 폴더
라우팅을 담당할 routes 폴더로 나뉜다.

또한 Micro Front-End multirepo를 사용시 repos 폴더를 추가적으로 구성할 수 있습니다.

플러터에서 멀티리포가 궁금하다면
https://kauemurakami.medium.com/micro-front-ends-com-flutter-872246bb9eec

트리로 보면 쉽게 이해가 갑니다. 다음은 /lib 폴더 모습입니다.

- main.dart   
- /lib/app
    - /data
        - /enums 
        - /services
            - /example_service.dart
                - service.dart
                - repository.dart
        - /provider
            - api_provider.dart
            - db_provider.dart
            - storage_provider.dart
        - /model
            - model.dart
    - /modules
        - /my_module
            - page.dart
            - controller.dart
            - binding.dart
            - repository.dart
            - /local_widgets
    - /global_widgets 

- /routes
    - routes.dart
    - pages.dart
- /core
    - /errors
    - /values
        - strings.dart
        - colors.dart
        - /languages
            - /from
                - pt-br.dart
                - en-au.dart
    - /theme
        - text_theme.dart  
        - color_theme.dart  
        - app_theme.dart  
    - /utils
        - /extensions
            - example_remove_underlines.dart
            
        - /functions
            - get_percent_size.dart
            
        - /helpers
            - masks.dart
            - keys.dart 
- /repos
    - /dependencies
    - /core
    

트리로 보면 직관적으로 감이 올 것이다.
그럼 하나하나 예시를 보여 살펴보겠다.

예시에 [1] 대괄호를 붙여 트리의 뎁스를 표현하겠다.

/lib/app - [1]

앱이 작동하는데 필요한 모든 모듈과 데이터를 포함한다.
app 폴더는 /data, /modules, /widgets 3개의 하위 폴더로 나뉜다.

/data - [2]

model 클래스와 api와 serivce 데이터와 관련된 모든 것을 포함한 디렉터리이다.
데이터 폴더는 4개의 하위 폴더로 정의한다.
/models, /providers, /services, /enums

그냥 일반적인 데이터 관련된 서비스단 코드가 들어가기에 논의할게 별로 없다.


/enums - [3]

enum 클래스를 정의하는 폴더이다.

  • animals.dart
enum Animals { Todos, Gatos, Cachorros }

/models - [3]

데이터 model 클래스를 정의하는 폴더이다.
이것 또한 별다른 코멘트가 필요없다.

  • animals.dart
// To parse this JSON data, do
//
//     final animals = animalsFromJson(jsonString);

import 'dart:convert';

List<Animals> animalsFromJson(str) =>
    List<Animals>.from(str.map((x) => Animals.fromJson(x)));

String animalsToJson(List<Animals> data) =>
    json.encode(List<dynamic>.from(data.map((x) => x.toJson())));

class Animals {
  Animals({
    this.breeds,
    this.id,
    this.url,
    this.width,
    this.height,
  });

  List<dynamic>? breeds;
  String? id;
  String? url;
  dynamic width;
  dynamic height;

  factory Animals.fromJson(Map<String, dynamic> json) => Animals(
        breeds: List<dynamic>.from(json["breeds"].map((x) => x)),
        id: json["id"],
        url: json["url"],
        width: json["width"],
        height: json["height"],
      );

  Map<String, dynamic> toJson() => {
        "breeds": List<dynamic>.from(breeds!.map((x) => x)),
        "id": id,
        "url": url,
        "width": width,
        "height": height,
      };
}
  • user.dart
class User {
  String? email, senha;
  User({this.email, this.senha});
}

/provider - [3]

데이터 공급자를 정의한다. API, 로컬 DB, 파이어베이스 정도 일 것이다.
여기 폴더에서 비동기 요청, HTTP 등 통신을 담당한다.

"provider"라는 용어는 다양한 방식에서 접근할 수 있지만 여기서는 http 요청을 하거나 DB간 지속성 있는 통신을 위해 존재한다.

단일 파일에 많은 요청이 있는 경우 엔티티별로 구분할 수 있다.
이는 프로그래머가 유도리있게 정의하면 된다.

  • example_api.dart
import 'dart:convert';

import 'package:example/app/data/models/animals.dart';
import 'package:example/app/data/models/app_error.dart';
import 'package:example/app/data/models/user.dart';
import 'package:example/core/utils/headers_api.dart';
import 'package:example/core/values/consts.dart';
import 'package:get/get_connect/connect.dart';

const catsUrl = 'https://api.thecatapi.com/v1/images/search';
const dogsUrl = 'https://api.thedogapi.com/v1/images/search';

class MyApi extends GetConnect {
  login(String _) async {
    bool? exists;
    exists = jsonUsers['users']!.contains(_);
    return exists ? User(email: _) : AppError(message: 'E-mail não existe');
  }

  getCats() async {
    final _ = await get('$catsUrl/?limit=20&page=1&order=desc',
        decoder: (_) => _,
        headers: HeadersAPI(apiKey: CAT_API_KEY).getHeaders());
    if (_.hasError) {
      return AppError(message: 'Algum erro inesperado aconteceu');
    } else {
      return animalsFromJson(_.body);
    }
  }

  getDogs() async {
    final _ = await get('$dogsUrl/?limit=20&page=1&order=desc',
        decoder: (_) => _,
        headers: HeadersAPI(apiKey: DOG_API_KEY).getHeaders());

    if (_.hasError) {
      return AppError(message: 'Algum erro inesperado aconteceu');
    } else {
      return animalsFromJson(_.body);
    }
  }

  getAll() async {
    var list;
    await Future.wait<dynamic>([getCats(), getDogs()]).then((value) {
      list = value.first;
      list.addAll(value.last);
    });
    list.sort((a, b) => a.hashCode.compareTo(b.hashCode));
    return list;
  }
}

/services - [3]

마지막으로 나올 건 당연히 서비스이다! 여러 서비스를 정의한 폴더이다. 이 폴더에서 정의되는 클래스는 레포지토리 컨트롤러 데이터 간의 통신을 관리하는 클래스 뿐이다. 컨트롤러는 데이터의 출처를 알 필요 없고 필요한 경우 컨트롤러에서 둘 이상의 레포지토리를 사용할 수 있다.
레포지토리는 엔티티로 구분되어야 하고 보통 DB 테이블 기반으로 한다. 클래스 내부에는 로컬 API 또는 DB에서 데이터를 요청하는 함수가 포함된다.

각 서비스는 repository.dart와 service.dart 파일을 갖는다.

repository.dart는 해당 모듈의 컨트롤러에서 사용하는 provider의 기능을 그룹화하는 일만 담당한다.

/services/example_app_config - [4]

  • ./repository.dart
import 'package:example/app/data/provider/api.dart';

class AppConfigRepository {
  final MyApi api;

  AppConfigRepository(this.api);
}
  • ./service.dart
import 'package:example/app/data/provider/api.dart';
import 'package:example/app/data/services/app_config/repository.dart';
import 'package:example/core/values/consts.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';

class AppConfigService extends GetxService {
  late AppConfigRepository repository;
  late GetStorage box;
  Future<AppConfigService> init() async {
    repository = AppConfigRepository(MyApi());
    box = GetStorage();
    await box.writeIfNull(IS_LOGGED, false);
    await box.writeIfNull(DARK_MODE, false);
    await box.writeIfNull(USER_EMAIL, '');
    return this;
  }

  darkMode() => box.read(DARK_MODE);
  isLogged() => box.read(IS_LOGGED);
  useremail() => box.read(USER_EMAIL);

  changeDarkMode(_) async {
    Get.changeThemeMode(Get.isDarkMode ? ThemeMode.light : ThemeMode.dark);
    Get.changeTheme(Get.isDarkMode ? ThemeData.light() : ThemeData.dark());
    await box.write(DARK_MODE, _);
  }

  changeIsLogged(_) async => box.write(IS_LOGGED, _);
  changeUserEmail(_) async => box.write(USER_EMAIL, _);
}

/services/example_auth - [4]

  • ./repository.dart
import 'package:example/app/data/provider/api.dart';

class AuthRepository {
  final MyApi api;

  AuthRepository(this.api);

  login(_) => api.login(_);
}
  • ./service.dart
import 'package:example/app/data/models/user.dart';
import 'package:example/app/data/provider/api.dart';
import 'package:example/app/data/services/auth/repository.dart';
import 'package:get/get.dart';

class AuthService extends GetxService {
  late AuthRepository repository;
  Future<AuthService> init() async {
    repository = AuthRepository(MyApi());
    return this;
  }

  final user = User().obs;

  login(_) async => await repository.login(_);
}

언급한 봐와 같이 각 서비스에 repository.dart와 service.dart 파일을 정의한다.
직관적이라 더 설명이 필요 없을 것 같다.

/app/data는 이렇게 끝이다.
이제는 /app/modules를 살펴보자.

/modules - [2]

각 모듈은 page와 해당 GetXController 그리고 이에 대한 종속성 혹은 바인딩으로 구성한다.
GetX 패턴에선 각 화면을 독립 모듈로 취급한다. 유일한 컨트롤러가 있고 종속성도 포함할 수 있기 때문이다.

모듈에서 재사용 가능한 위젯을 사용할 경우 해당 위젯에 대한 폴더를 추가할 수도 있다.
각 모듈은 폴더별로 존재하고 그 안에 다음과 같은 파일이 정의된다.

/my_module

  • page.dart
  • controller.dart
  • binding.dart
  • repository.dart
  • /widgets (로컬 위젯은 선택사항)

각 dart파일은 예시와 함께 설명하겠다.


/login_module - [3]

  • page.dart

GetView extneds 한다.

import 'package:example/app/modules/login/controller.dart';
import 'package:example/app/modules/login/widgets/form.dart';
import 'package:example/app/modules/login/widgets/top_section.dart';
import 'package:example/core/utils/functions/size_config.dart';

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

class LoginPage extends GetView<LoginController> {
  GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  @override
  Widget build(BuildContext context) {
    SizeConfig().init(context);

    return Scaffold(
        body: SafeArea(
            child: Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          const TopSectionWidget(),
          Expanded(child: Container()),
          Expanded(
              flex: 3,
              child: LoginFormwidget(
                formkey: _formKey,
              )),
        ],
      ),
    )));
  }
}

  • binding.dart
    바인딩 클래스는 상태 관리자와 종속성 관리자에 대한 경로를 바인딩 하면서 종속성 주입을 분리하는 클래스이다.

종속성 관리에서 이상적인 바인딩은 GetView를 사용하여 View에서 직접 호출하지 않고도 controller와 repository, API 및 필요한 모든 것을 초기화하는 것이다.

여기서 특정 controller를 사용할 때 어떤 화면에 표시되어야 하는지 어디에 어떻게 버려야 하는 지 알 수 있다. 또한 바인딩 클래스를 사용하면 SmartManager 구성과 제어가 가능하다.
종속성을 구성하는 방법을 정의하고 스택에서 경로를 제거하거나 배치에 위젯을 사용하거나 아무 것도 사용하지 않을 대 구성할 수 있다.

import 'package:example/app/data/provider/api.dart';
import 'package:example/app/modules/login/controller.dart';
import 'package:example/app/modules/login/repository.dart';
import 'package:get/get.dart';

class LoginBinding implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut<LoginController>(
        () => LoginController(LoginRepository(MyApi())));
  }
}

  • repository.dart
    각 모듈 내에서 따로 전역적으로 repository를 사용한 이유는 다른 모듈에서 함수를 사용할 수 있지만 문제는 controller에서 둘 이상의 repository를 가져와야 했기 때문이다.

따라서 함수, 내부 repository를 반복할 수 있으므로 더 빠른 유지 관리와 모듈에 연관된 모든 것에 접근할 수 있다.

결국 repository는 모듈의 컨트롤러를 가리키는 클래스가 된다. 우리가 사용할 provider와 service도 마찬가지이다. 일부 provider와 service는 자체 repository가 있어야 한다.

import 'package:example/app/data/provider/api.dart';

class LoginRepository {
  final MyApi api;

  LoginRepository(this.api);
}

  • controller.dart

컨트롤러는 앱의 중요한 부분이다. 상태관리할 값을 .obs변수로 생성한다.
또한 컨트롤러는 우리가 앞서 data폴더에 정의한 기능들을 직접적으로 사용하는 곳이다.
강조하지만 repository는 provider의 호출만 수행한다.

컨트롤러에는 규칙이 있는데
"모든 컨트롤러에는 GetX 위젯에서 컨트롤러를 초기화 하는데 필요한 속성인 repository가 하나만 있어야 한다" 이다.

만약 동일한 페이지에 있는 서로 다른 두 repository의 데이터가 필요한 경우 두 개의 GetX 위젯을 사용해야 한다. 각 페이지에 대해 하나 이상의 컨트롤러가 있는 것이 좋다.

여러 page에 동일한 controller를 사용할 수 있도록 한 가지 예외가 있다.
중요한 점은 모든 page의 데이터가 단일 repository를 사용하는 경우에만 여러 페이지에서 controller를 독점적으로 사용할 수 있다.

이렇게 하는 이유는 GetX의 장점을 최대한 활용하도록 하기 위함이다. 두 엔티티를 조작해야 할 때마다 두 개의 다른 컨트롤러와 view가 필요하다.

두 개의 repository가 있는 controller가 있고 해당 controller가 두 reopository에서 controller가 검색한 데이터를 사용하여 page의 GetX 위젯과 함께 사용된다고 생각해보자.

엔티티가 수정될 때마다 controller는 두 변수를 담당하는 위젯을 업데이트해야 하며 그 중 하나는 변경할 필요가 없다. 따라서 컨트롤러별로 repository를 분리하면 GetX위젯으로 작업할 때 각 위젯에 대해 책임 있는 컨트롤러를 갖고 이 정보를 표시하며 .obs 변수가 변경된 위젯만 렌더링 하는 것이 좋은 방법이 될 수 있다.

import 'package:example/app/data/models/user.dart';
import 'package:example/app/data/services/app_config/service.dart';
import 'package:example/app/data/services/auth/service.dart';
import 'package:example/app/modules/login/repository.dart';
import 'package:example/core/utils/functions/verify_response.dart';
import 'package:example/routes/pages.dart';
import 'package:get/get.dart';

class LoginController extends GetxController {
  final LoginRepository repository;
  LoginController(this.repository);
  final user = User().obs;
  final isEmail = false.obs;
  AppConfigService? config;
  AuthService? auth;
  final darkMode = false.obs;

  @override
  void onInit() async {
    config = Get.find<AppConfigService>();
    auth = Get.find<AuthService>();
    darkMode.value = config!.darkMode();
    await reauth();
    super.onInit();
  }

  changeTheme() {
    config!.changeDarkMode(Get.isDarkMode ? false : true);
    darkMode.value = config!.darkMode();
  }

  reauth() async {
    await Future.delayed(Duration.zero, () {
      if (config!.isLogged()) {
        Get.offNamed(Routes.HOME);
      }
    });
  }

  login() async {
    final _ = await auth!.login(user.value.email);
    if (verifyresponse(_)) {
      Get.showSnackbar(GetSnackBar(
        message: _.message,
        duration: const Duration(seconds: 2),
      ));
    } else {
      config!.changeIsLogged(true);
      Get.offNamed(Routes.HOME);
    }
  }

  onChangeEmail(_) {
    GetUtils.isEmail(_) ? isEmail.value = true : isEmail.value = false;

    user.update((val) => val!.email = _);
  }

  onSavedEmail(_) => user.update((val) => val!.email = _);
  onValidateEmail(_) => GetUtils.isEmail(_) ? null : 'Insira um email válido';
}

/widgets(전역) - [2]

여러 모듈에서 사용할 widget들은 여기에 정의하면 된다.
여기까지가 lib/data 폴더이다.


/lib/routes - [1]

routes 폴더에는 routes.dart와 pages.dart 파일을 정의한다.
routes.dart는 상수 라우팅 정보를 정의한 것이고
pages.dart는 실제 라우팅을 정의한 클래스이다.

  • routes.dart
part of './pages.dart';

abstract class Routes {
  static const INITIAL = '/';
  static const HOME = '/home';
  static const LOGIN = '/login';
  static const ANIMAL_DETAILS = '/animal-details';
}
  • pages.dart
import 'package:example/app/modules/animal_details/binding.dart';
import 'package:example/app/modules/animal_details/page.dart';
import 'package:example/app/modules/home/binding.dart';
import 'package:example/app/modules/home/page.dart';
import 'package:example/app/modules/login/binding.dart';
import 'package:example/app/modules/login/page.dart';
import 'package:get/get.dart';

part './routes.dart';

abstract class AppPages {
  static final pages = [
    GetPage(
        name: Routes.HOME,
        page: () => const HomePage(),
        bindings: [HomeBinding()]),
    GetPage(
        name: Routes.LOGIN,
        page: () => LoginPage(),
        bindings: [LoginBinding()]),
    GetPage(
        name: Routes.ANIMAL_DETAILS,
        page: () => const AnimalDetailsPage(),
        bindings: [AnimalDetailsBinding()]),
  ];
}

라우팅 부분은 간단하다.


/lib/core - [1]

코어에는 앱의 핵심 구성들을 정의한다. 데이터베이스의 config 파일, 테마, 언어, 스타일에 대한 상수, 컬러 값, 정적 문자열 등도 여기에 정의 된다.

/errors - [2]

에러 핸들링 관련한 클래스를 여기에 정의하면 된다.

/values - [2]

앱에 다양한 정적 값들을 모아둔 폴더이다.
전역에서 사용할 여러 문자열을 모아두면 된다.

  • strings.dart
String ENTER = 'Enter';
String EMAIL = 'E-mail';
  • colors.dart
import 'package:flutter/material.dart';

final Color tumbleweed = Colors.purple.shade400;
const Color desert_sand = Color(0xfff6d4ba);
const Color cornsilk = Color(0xfffefadc);
const Color white_smoke = Color(0xffF5F5F5);
const Color middle_yellow = Color(0xfffeea00);
const Color bg_color = Color(0xff121212);
const Color greenzin = Color(0xff7acbb5);
  • consts.dart

당연하 colors, strings 외에 개발자가 유동적으로 추가해서 사용하면 된다.

const IS_LOGGED = 'is_logged';
const DARK_MODE = 'dark_mode';
const USER_EMAIL = 'user_email';

const jsonUsers = {
  "users": ["kauetmurakami@gmail.com", "email@gmail.com", "usuario@gmail.com"],
};

const IMAGES = 'assets/images/';

const CAT_API_KEY = 'cfc155ea-de7b-407a-beab-e34e161232ae';
const DOG_API_KEY = 'cfd92bfa-f212-4a73-8792-5ef7271a0045';

laguages - [3]

다국어를 지원하는 앱일 경우 여기에 번역 파일을 정의하면 된다.
플러터 다국어 관련해서 검색하면 방법을 찾을 수 있을 것이다.


theme - [2]

여기에서 위젯, 텍스트 및 색상에 대한 테마를 만들 수 있다.
개발자가 다양하게 커스터마이징 하면 된다.

  • text_theme.dart
import 'package:flutter/material.dart';

const text_white = TextStyle(color: Colors.white);

  • color_theme.dart
final colorCard = Color(0xffEDEDEE)

  • app_theme.dart
final textTheme = TextTheme(headline1: TextStyle(color: colorCard))

utils - [2]

여기에서 앱에 유틸리티를 추가하면 된다.

extensions - [3]

기존 라이브러리에 기능을 추가할 때 여기 폴더에 정의하면 된다.
공식 문서를 참고하자 https://dart.dev/language/extension-methods

  • example_remove_underlines.dart
  • ...

functions - [3]

앱에서 전역으로 사용할 함수들을 정의한다.

  • size_config.dart
import 'package:flutter/widgets.dart';

class SizeConfig {
  static MediaQueryData? _mediaQueryData;
  static double? screenHeight, wPercent, hPercent, screenWidth;

  void init(BuildContext context) {
    _mediaQueryData = MediaQuery.of(context);
    screenWidth = _mediaQueryData!.size.width;
    screenHeight = _mediaQueryData!.size.height;
    wPercent = screenWidth! / 100;
    hPercent = screenHeight! / 100;
  }

  static double hp(y) => y * hPercent;

  static double wp(x) => x * wPercent;
}

percentW(x) => SizeConfig.wp(x);
percentH(y) => SizeConfig.hp(y);
  • verify_response.dart
import 'package:example/app/data/models/app_error.dart';

verifyresponse(_) {
  if (_.runtimeType == AppError) {
    return true;
  } else {
    return false;
  }
}

helpers - [3]

key나 mask 등과 같이 추상 클래스 또는 헬퍼 클래스는 정의한다.

  • masks.dart
static final maskCPF = MaskTextInputFormatter(mask: "###.###.###-##", filter: {"#": RegExp(r'[0-9]')});

  • keys.dart
static final GlobalKey formKey = GlobalKey<FormState>();

  • headers_api.dart
class HeadersAPI {
  final apiKey;

  HeadersAPI({this.apiKey});
  Map<String, String> getHeaders() {
    print({
      "Content-Type": "application/json",
      "Accept-Charset": "UTF-8",
      "x-api-key": apiKey
    });

    return {
      "Content-Type": "application/json",
      "Accept-Charset": "UTF-8",
      "x-api-key": apiKey
    };
  }
}

여기까지가 끝이다. main은 평범하다.

/lib/main.dart

import 'package:example/app/data/services/app_config/service.dart';
import 'package:example/app/data/services/auth/service.dart';
import 'package:example/app/modules/login/binding.dart';
import 'package:example/app/modules/login/page.dart';
import 'package:example/routes/pages.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await GetStorage.init();
  await Get.putAsync(() => AppConfigService().init());
  await Get.putAsync(() => AuthService().init());

  runApp(GetMaterialApp(
    initialBinding: LoginBinding(),
    initialRoute: Routes.LOGIN,
    getPages: AppPages.pages,
    theme: ThemeData.light(),
    darkTheme: ThemeData.dark(),
    debugShowCheckedModeBanner: false,
  ));
}

이런 느낌이다.

다들 즐거운 개발하세요.

profile
태호입니다.

1개의 댓글

comment-user-thumbnail
2024년 3월 14일

좋은 글 감사합니다~

답글 달기