ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ flutter ]상태관리의 끝판왕? GetX를 정리해 보았다.
    개발일지/flutter 2021. 1. 30. 11:53

    안녕하세요 개발하는 남자 개남입니다 :) 

    오늘은 GetX에 대해 정리하는 포스팅을 써보려고 합니다.

     

     

    처음 플러터로 개발하면서 상태 관리를 위해 접했던 것이 bloc이었습니다.

    그 당시 정말 강력하다고 생각했었지만 실무에 사용하면서 점점... 상태 관리를 위해 사전에 만들어야 할 클래스, 이벤트 등, 선 작업이 많아지고 불편함을 느껴오고 있었습니다. 그러다 provider를 알게 되고 완전 신세계를 경험하게 됩니다. 지난 포스팅 bloc보다 provider라는 포스팅을 쓸 정도로 provider의 강점을 느꼈고 실무에도 적용할 정도로 너무 좋았었습니다.

    근데 페이스북을 통해 GetX라는 상태관리의 끝판왕급이라는 소문으로 GetX가 있다는 이야기만 들었습니다.

    사실 provider를 잘 사용해서 앱도 만들고 출시도 한 터라 GetX를 사용하지 않아도 충분함을 느꼈지만, 플러터 라이브러리 중 TOP3의 인기 있는 플러그인을 공부해 보고 싶어 져서 공부하고 사용을 해봤습니다.

     

    결과는? 

    신세계 + 신세계 급이었습니다.

    provider 를 쓰면서 불필요한 코드들... bloc에 비해 많이 줄었다고 생각했는데 이마저도 줄여버리는 GetX!!

    심지어 한국어 문서도 있다는 사실 문서 확인은 아래 링크 클릭 :) 

     

    jonataslaw/getx

    Open screens/snackbars/dialogs/bottomSheets without context, manage states and inject dependencies easily with Get. - jonataslaw/getx

    github.com

     


    GetX는?

    Flutter를 위한 매우 가볍고 강력한 라이브러리이며 강력함을 뒷밭힘 하고 있는 3가지 기본 원칙이 있으며 다음과 같습니다.

    첫째. 생산성 

     - 같은 기능도 더욱더 편하고 간결하게 표현이 가능합니다. 

     - 컨트롤러들을 사용하고 반환시켜주는 처리를 신경 쓰지 않아도 알아서 GetX에서 사용되지 않을 때 제거해주기 때문에 개발자분들은 더욱더 개발에만 신경 쓸 수 있습니다.

    둘째. 성능

     - GetX는 성능과 최소한의 리소스 소비에 중점을 둡니다.

     - GetX는 Streams나 ChangeNotifier를 사용하지 않습니다.

     - 최소의 재 빌드를 위해 똑똑한 알고리즘을 적용하기 위해, GetX는 상태가 변했는지 확인하는 comparator를 사용합니다.

    셋째. 조직화  

     - GetX는 화면, 비즈니스 로직, 종속성 주입 및 내비게이션을 완전히 분리하여 관리할 수 있습니다.

     -  GetX는 자체 종속성 주입 기능을 사용하여 DI를 뷰에서 완전히 분리하기 때문에 다중 Provider를 통해 위젯 트리에서 컨트롤러/모델/블록으로 주입할 필요가 없습니다

     - GetX를 사용하면 기본적으로 클린 코드를 가지게 되어 애플리케이션의 각 기능을 쉽게 찾을 수 있습니다. 이것은 유지 보수를 용이하게 하며 모듈의 공유가 가능하고 Flutter에서는 생각할 수 없었던 것들도 전부 가능합니다.

     

     

    GetX를 사용하기 위한 사전 세팅

    pubspec.yaml 파일에 다음 소스 추가로 프로젝트에 라이브러리를 추가해줍니다.

    dependencies:
      get: ^3.24.0

    기본 프로젝트를 생성하면 다음과 같이 main함수가 작성되어있다.

    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: Home(),
        );
      }
    }
    

     

    이제 GetX에서 프로젝트의 모든 것을 관리할 수 있도록 설정하려면 MaterialApp을 사용하는 대신 앞에 Get만 추가해주면 된다.

    다음 코드가 위의 코드를 Get에서 관리될 수 있도록 변경한 소스입니다.

    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return GetMaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: Home(),
        );
      }
    }
    

     

    더욱 간결하게 작성하려면 다음과 같이 한 줄로 작성할 수 있습니다. 

    void main() => runApp(GetMaterialApp(home: Home()));

    GetMaterialApp을 사용하지 않아도 충분히 상태 관리는 할 수 있다고 합니다. 

    GetMaterialApp을 사용함으로써 라우트 관리, 그 밖의 기능들(스낵바, 국제화, bottomSheets , 다이얼로그 등)을 사용할 수 있다고 합니다.

    즉, GetX를 상태 관리 목적으로만 사용하실 분이라면 GetMaterialApp을 사용하지 않아도 된다는 것입니다.

     

    위 와 같이 세팅을 해놨다면 이제 GetX를 사용할 준비는 모두 완료가 된 것입니다.


    GetX가 말하는 세 가지 주요점

    첫째. 상태 관리

    GetX는 두 가지 상태 관리자가 있습니다: 단순 상태 관리자(GetBuilder라고 함)와 반응형 상태 관리자(GetX/Obx)


    단순 상태 관리자(GetBuilder)

      - 이는 기존의 ChangeNotifierProvider와 거의 비슷하다고 볼 수 있습니다. 단편적인 예로 provider와 getx의 단 순 상태 관리를 비교하겠습니다. 

     

    다음은 provider의 방식입니다.

    //controller 상태관리 이후 brodcast 방법
    class ChangeNotifierController extends ChangeNotifier{
    	int count=0;
        void increment(){
        	count++
            notifyListeners();
        }
    }
    //변경된 상태값을 전달받는 ui 쪽 소스
    Consumer<ChangeNotifierController>( 
      builder: (context, controller, child) { 
      	return Text( '${controller.count}');
      }
    ),
    

    다음은 GetX의 단순 상태 관리 방식입니다.

    //controller 상태관리 이후 brodcast 방법
    class CounterController extends GetxController{
    	int count=0;
        void increment(){
        	count++
            update();
        }
    }
    //변경된 상태값을 전달받는 ui 쪽 소스
    Getbuilder<CounterController>( 
      builder: (controller) { 
      	return Text( '${controller.count}');
      }
    ),
    

     

    두 가지 소스가 거의 비슷함을 알 수 있습니다.

    하지만 GetX에는 반응형 상태 관리자라는 것이 있습니다.

     

    반응형 상태 관리자(GetX/Obx)

     - Obx는 단순 상태 관리에 비해 다소 복잡하기 때문에 사용을 꺼리게 될 수도 있습니다,

     하지만 반응형 상태 관리를 사용하게 되면 상태관리에 대해서 리소스를 아낄 수 있으며, 말 그대로 반응적으로 상태관리를 할 수 있습니다.

     예) 만약 name "John"이고, 당신이 이를 "John" (name.value = "John")으로 바꾼다면, 기존 value와 동일하므로 화면상으로 바뀌는 것이 없습니다. 그리고 Obx는 리소스를 아끼기 위해 새 값을 무시하고 재 빌드하지 않습니다.

     

    위의 단순 상태 관리의 소스를 반응형 상태 관리로 변경해보겠습니다.

    //controller 상태관리 이후 brodcast 방법
    class CounterController extends GetxController{
        RxInt count=0.obs;
        void increment(){
        	count++;
        }
    }
    //변경된 상태값을 전달받는 ui 쪽 소스
    Obx(()=> Text( '${controller.count}'))
    

     

    소스도 매우 간결해진 것을 볼 수 있습니다. 

    여기서 볼 때 int 타입이 RxInt로 변경되며 0 값 옆에 obs라는 것이 붙게 되는데 이것이 반응형 상태 관리를 위한 변수 선언 방법입니다.

    변수를 "observable"하게 만드는 방법에는 3가지로 할 수 있습니다.

     

    첫 번째 타입 선언 방법 Rx{Type}

    // 초기값을 설정하는 것을 추천하지만, 필수는 아닙니다.
    final name = RxString('');
    final isLogged = RxBool(false);
    final count = RxInt(0);
    final balance = RxDouble(0.0);
    final items = RxList<String>([]);
    final myMap = RxMap<String, int>({});

    두 번째 타입:  Rx<Type>

    final name = Rx<String>('');
    final isLogged = Rx<Bool>(false);
    final count = Rx<Int>(0);
    final balance = Rx<Double>(0.0);
    final number = Rx<Num>(0);
    final items = Rx<List<String>>([]);
    final myMap = Rx<Map<String, int>>({});
    
    // 커스텀 클래스 - 그 어떤 클래스도 가능합니다
    final user = Rx<User>();

    세 번째 방법: 단순히 **.obs**를 value의 속성으로 덧붙이는 방법

    final name = ''.obs;
    final isLogged = false.obs;
    final count = 0.obs;
    final balance = 0.0.obs;
    final number = 0.obs;
    final items = <String>[].obs;
    final myMap = <String, int>{}.obs;
    
    // 커스텀 클래스 - 그 어떤 클래스도 가능합니다
    final user = User().obs;

    이중 사용하기 편리하고 흔히들 사용하는 방법이 세 번째 방법입니다. 저 역시 세 번째 방법으로 사용합니다.

    그다음에 눈에 들어오는 것이 Obx() 형태의 위젯입니다.

    다른 Builder 나 GetX 나 GetBuilder와 다른 것은 상태를 모니터링을 할 컨트롤러를 선언하지 않는다는 것입니다.

    즉 Obx() 안에 포함된 모든 "observable"을 모니터링하여 변경된 것을 감지하여 업데이트를 진행 합니다.

    단, 단순 상태 관리처럼 update()를 통해 변경된 것은 observable아 아니기 때문에 변경되지 않습니다.

     

    다음으로 반응형 상태 관리를 통해 선언한  "observable" 은 Worker이벤트를 등록할 수 있습니다.

    /// 'count1'이 변경될 때마다 호출
    ever(count1, (_) => print("$_ has been changed"));
    
    /// 'count1'이 처음으로 변경될 때 호출
    once(count1, (_) => print("$_ was changed once"));
    
    /// Anti DDos - 'count1'이 변경되고 1초간 변화가 없을 때 호출
    debounce(count1, (_) => print("debouce$_"), time: Duration(seconds: 1));
    
    /// 'count1'이 변경되고 있는 동안 1초 간격으로 호출
    interval(count1, (_) => print("interval $_"), time: Duration(seconds: 1));

    Worker는 Controller 혹은 클래스를 시작할 때만 사용할 수 있습니다. 그래서 항상 onInit 내에 있거나(권장사항), 클래스 생성자, StatefulWidget의 initState 안에(권장하지는 않지만 부작용은 없습니다.) 있어야 합니다.


    둘째. 라우트 관리

     

    flutter route를 사용하면서 간단하게 페이지 변경하는 데에도 MaterialPageRoute를 사용해야 하고 무엇보다도 context를 담아 줘야 했기 때문에 불필요하게 context를 전달하게 된 경험도 있었고 사용하면서 불필요한 소스를 왜 써야 하는지 불만을 갖고 있었습니다. 이런 불만을 Getx에서 한방에 해결이 가능합니다.

     

    기존의 페이지 이동하기 위한 소스 

    Navigator.of(context).push(
      MaterialPageRoute(
     	 builder: (BuildContext context) => HomePage(),
      ),
    );

    Getx에서 페이지 이동을 위한 소스

    Get.to(HomePage());

    4줄짜리 코드를 한 줄로 바꿔주는 놀라운 매직 같은 Getx라고 할 수 있습니다.

     

    페이지 이동을 하면서 페이지 전환 효과 역시 지정해줄 수 있습니다. 

     Get.to(SamplePage(), transition: Transition.downToUp);

    Transition에 정의는 다음과 같이 다양하게 지원합니다. 

    enum Transition {
      fade,
      fadeIn,
      rightToLeft,
      leftToRight,
      upToDown,
      downToUp,
      rightToLeftWithFade,
      leftToRightWithFade,
      zoom,
      topLevel,
      noTransition,
      cupertino,
      cupertinoDialog,
      size,
      native
    }
    

    라우트 이름이 있고 없고에 따라 사용방법을 다양하게 지원합니다.

    이름이 있는 라우트 관리 

    Get.toNamed("/NextScreen");

    이름이 없는 라우트 관리

    Get.to(NextScreen());

     

    페이지 이동시 history 관리 역시 가능합니다. 

    다음 화면으로 이동하고 이전 화면에서 돌아오지 않는 경우 (스플래시나 로그인 화면 등을 사용하는 경우)

    //flutter navigator 사용시 
    Navigator.of(context).pushReplacement(
    	MaterialPageRoute(
    		builder: (BuildContext context) => NextScreen(),
    	),
    );
    //or
    Navigator.of(context).pushReplacementNamed("/NextScreen");
    
    //GetX 사용시 
    Get.off(NextScreen());
    //or
    Get.offNamed("/NextScreen");

    다음 화면으로 이동하고 이전 화면이 모두 닫히는 경우 (장바구니, 투표, 테스트에 유용함)

    //flutter navigator 사용시 
    Navigator.of(context).pushAndRemoveUntil(
      MaterialPageRoute(
      	builder: (context) => GetViewWidget(),
      ),
      (Route<dynamic> route) => false,
    );
    //or
    Navigator.of(context).pushNamedAndRemoveUntil(
    "/NextScreen", (Route<dynamic> route) => false);
    
    
    //GetX 사용시
    Get.offAll(NextScreen());
    //or
    Get.offAllNamed("/NextScreen");

    그냥 눈으로만 봐도 뭘 쓰는 것이 좋을지는 답이 딱 나온 것 같습니다.

     

    라우트 정의하는 방법

    //flutter 기존 route 정의
    void main() {
      runApp(
      	MaterialApp(
        initialRoute: '/',
        routes: {
          '/': (context) => MyHomePage(),
          '/second': (context) => Second(),
          '/third': (context) => Third(),
        },
    }
    
    //GetX route 정의 
    void main() {
      runApp(
        GetMaterialApp(
          initialRoute: '/',
          getPages: [
            GetPage(name: '/', page: () => MyHomePage()),
            GetPage(name: '/second', page: () => Second()),
            GetPage(
              name: '/third',
              page: () => Third(),
              transition: Transition.zoom  
            ),
          ],
        )
      );
    }

    라우트에 데이터 보내기 

    무엇이든 인수를 통해 전달합니다. GetX는 String, Map, List, 클래스 인스턴스 등 모든 것을 허용합니다.

    Get.toNamed("/NextScreen", arguments: 'Get is the best');

    넘겨받은 페이지 위젯에서 arguments 사용방법

    print(Get.arguments);

    기존의 flutter에 pushName으로 보내고 받기 위해서는 설정해야 할 것이 다소 복잡합니다.

    (설명은 생략하도록 하겠습니다.)

     

    동적 링크

    GetX는 웹과 같이 향상된 동적 url을 제공합니다. 웹 개발자들은 아마 Flutter에서 이미 이 기능을 원하고 있을 것입니다. 대부분의 경우 패키지가 이 기능을 약속하고 URL이 웹에서 제공하는 것과 완전히 다른 구문을 제공하는 것을 보았을 것입니다. 하지만 GetX는 이 기능을 해결합니다.

    Get.offAllNamed("/NextScreen?device=phone&id=354&name=Enzo");

    넘겨받은 페이지 위젯에서 url params로 받은 값을 사용방법

    print(Get.parameters['id']);
    // 출력: 354
    print(Get.parameters['name']);
    // 출력: Enzo

    GetX는 쉽게 NamedParameters 전달을 할 수 있습니다:

    void main() {
      runApp(
        GetMaterialApp(
          initialRoute: '/',
          getPages: [
          GetPage(
            name: '/profile/',
            page: () => MyProfile(),
          ),
           //You can define a different page for routes with arguments, and another without arguments, but for that you must use the slash '/' on the route that will not receive arguments as above.
           GetPage(
            name: '/profile/:user',
            page: () => UserProfile(),
          ),
         ],
        )
      );
    }

    위와 같이 route 정의를 내리고 

    Get.toNamed("/profile/34954");

    위와 같이 보내게 되면 UserProfile 페이지 위젯으로 routing  되며 그 페이지 위젯에서는 

    print(Get.parameters['user']);

    이처럼 parameters를 사용할 수 있습니다.


    셋째. 종속성 관리

     

    Get이나 Provider는 Controller를 주입하고 사용하기 위해서 종속 관리가 되어야 합니다. 

    Provider의 경우 context를 통해서 Controller를 찾을 수 있지만 GetX는 더 이상 context가 필요가 없습니다.

    //provider 종속성 인스턴스 선언
    MultiProvider(
      providers: [
      	ChangeNotifierProvider(create: (BuildContext context) =>Controller()),
      ],
    )
    
    //GetX 종속성 인스턴스 선언
    Get.put(Controller());

    사용법도 간단하며 무엇보다도 provider의 경우는 context에 의존하고 있어서 최상위에서 provider들을 모두 선언해줘야 맘 편하게 상태 관리를 할 수 있는 반면에 GetX는 context에 의존하지 않아서인지 Controller들을 사용할 때만 선언하고 제거해줄 수 있어서 편하고 좋았습니다.

     

    종속성 인스턴스 선언 방법에는 총 4가지 방식이 있습니다.

     - Get.put()

     - Get.lazyPut()

     - Get.putAsync()

     - Get.create()

     

    Get.put() 방식

    종속성 인스턴스화의 가장 흔한 방법입니다. 예를 들어 뷰의 controller들에 좋습니다.

    Get.put<SomeClass>(SomeClass());
    Get.put<LoginController>(LoginController(), permanent: true);
    Get.put<ListItemController>(ListItemController, tag: "some unique string");

    Get.lazyPut() 방식

    인스턴스 하게 사용하는 경우에만 의존성을 lazyLoad 할 수 있습니다. 계산 비용이 많이 드는 클래스나 한 곳에서 다양한 클래스를 당장 사용하지 않으면서 인스턴스화 하기를 원한다면(Bindings 클래스처럼) 매우 유용합니다.

    /// ApiMock은 처음으로 Get.find<ApiMock>을 사용하는 경우에만 호출됩니다.
    Get.lazyPut<ApiMock>(() => ApiMock());
    
    Get.lazyPut<FirebaseAuth>(
      () {
        // 어떤 로직이 필요하다면 ...
        return FirebaseAuth();
      },
      tag: Math.random().toString(),
      fenix: true
    )
    
    Get.lazyPut<Controller>( () => Controller() )

    Get.putAsync() 방식

    만약 비동기로 인스턴스를 등록하길 원하면 Get.putAsync를 사용할 수 있습니다.

    Get.putAsync<SharedPreferences>(() async {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setInt('counter', 12345);
      return prefs;
    });
    
    Get.putAsync<YourAsyncClass>( () async{
      //비동기 처리 await
      return Controller()
    });

    Get.create() 방식

    동일한 controller 같은 것을 종속성을 생성합니다. 

    예를 들어 리스트뷰 안의 버튼은 리스트를 위한 고유한 인스턴스입니다.

    Get.Create<SomeClass>(() => SomeClass());
    Get.Create<LoginController>(() => LoginController());

    개인적으로 사용해본 결과 Get.put과 Get.lazyPut의 경우는 자주 사용하면서 GetX의 힘을 경험했지만, putAsync와 create의 경우는 사용할 일이 없었습니다. 아니, 아직 사용해야 할 상황이나 정확한 이해를 하지 못해서 적용하지 못한 경우가 더 정확할 것입니다. 

    put, lazyPut 만 사용하더라도 서비스를 만드는데 큰 어려움이 없을 것으로 생각됩니다.

     

    이제 마지막으로 Binging을 다뤄 보겠습니다.

     

    바인딩

    바인딩은 페이지 이동, 즉 라우트와 연계되어 사용되는데 엄청난 장점이 있습니다.

    페이지에 필요한 컨트롤러를 바인딩하여 전달하면 페이지가 생성되면서 바로 인스턴스가 선언되어 사용 가능한 상태가 되고 페이지에서 빠져나오면 곧바로 해당 페이지에서 등록되어 사용되었던 인스턴스(컨트롤러)가 자동 삭제 처리가 됩니다. 페이지에서 스트림 , 타이머 등 사용 중이었다 해도 페이지에서 빠져나오면 곧바로 종료되기 때문에 신경 쓸 필요가 없다는 장점도 있습니다.

     

    바인딩을 사용하기 위해 class를 만들어서 사용하는 방법과 BindingsBuilder를 통해 class 만들지 않고 바로 사용하는 방법으로 나눌 수 있습니다.

     

    Class 파일을 만들어 사용하는 방법

    class HomeBinding implements Bindings {
      @override
      void dependencies() {
        Get.lazyPut<HomeController>(() => HomeController());
        Get.put<Service>(()=> Api());
      }
    }
    

    위와 같이 만들고 dependencies 의존성 부분에 필요한 컨트롤러나 서비스들을 정의해줍니다.

    GetPage(
        name: '/',
        page: () => HomeView(),
        binding: HomeBinding(),
      ),

    GetPage를 만들어 줄 때 binding 부분에 넣어주기만 하면 설정은 끝이 납니다.

    또는 Get.to를 사용하면서도 binding 처리를 해줄 수 있습니다.

    Get.to(Home(), binding: HomeBinding());

    BindingsBuilder를 사용해 class 없이 사용하는 방법

    GetPage(
        name: '/',
        page: () => HomeView(),
        binding: BindingsBuilder(() {
          Get.lazyPut<ControllerX>(() => ControllerX());
          Get.put<Service>(()=> Api());
        }),
      ),

    또는 

    Get.to(
      Home(),
      binding: BindingsBuilder(
        () {
          Get.lazyPut<ControllerX>(() => ControllerX());
          Get.put<Service>(() => Api());
        },
      ),
    );

     

    여기까지 GetX의 전반적인 기능들, 핵심적인 부분들을 정리하는 시간을 가져보았습니다.

    실무에서 적용해본 결과 provider 사용 때보다 더욱 ui와 비즈니스 로직과 분리가 완벽하게 되었다고 느껴지고 

    소스도 더욱 깔끔하게 작성을 할 수 있었던 것 같습니다.

     

    기술 관련 유튜브에서 위의 내용 그리고 이전 포스팅 내용들에 대해서 영상으로 확인 할 수 있습니다.

     

    댓글

Designed by Tistory.