[Flutter] GetX_pattern 익혀보기

leeeeeoy·2021년 9월 22일
8

이 글은 getx_pattern repository와 공식사이트를 보고 정리한 글입니다.

GetX_pattern이란

공식 사이트에는 GetX로 프로젝트를 표준화하기 위한 구조라고 설명한다. GetX는 Flutter의 여러 상태관리 라이브러리 중 하나로 간결하고 사용하기 쉬운 장점이 있다. 다만 BLoC이나 Provider에 비해 정해진 구조가 없기 때문에 등장한 것 같다. 한가지 흥미로운 점은 GetX 개발자와 GetX_pattern 개발자는 다른 사람이라는 것이다. 즉 GetX를 사용하면 꼭 이 pattern을 써야한다는 아니지만 여럿이 작업을 진행할 때는 정해진 구조를 따르는 것이 조금 더 나아보인다.

그동안 GetX를 사용하면서 page, controller, model, util 정도로 구분했는데 항상 Flutter 폴더 구조에 대해 고민하면서도 마땅한 답을 찾지 못했던 것 같다. 알게된 김에 정리해보자!

기본구조

공식 사이트를 보니 다음과 같이 Package 구조와 Module 구조가 있는 것 같다.

오른쪽이 Package, 왼쪽이 Module이다.

이렇게 보니까 마냥 복잡하기만 하다...그냥 쓰던대로 쓸 걸 그랬나 :(
사이트가 포르투갈어로 되어 있어서 번역기 돌리고 최대한 이해하려고 노력했다.
개인적인 생각으론 Package 구조가 조금 더 직관적인 것 같다 Package구조에 맞춰서 정리해보았다. 공식 사이트 순서대로 읽어가면서 정리했다.

폴더구조

lib
  ├── app
  │   │
  │   ├── data
  │   │	  ├── provider
  │   │	  ├── model
  │   │	  └── repository
  │   │
  │   ├── controller
  │   │
  │   ├── ui
  │   │
  │   ├── binding
  │   │
  │   └── routes
  │
  └── main.dart

기본적인 폴더 구조는 이렇다. 공식 사이트를 참조해 조금 간략하게 정리했는데 플랫폼별 위젯이나 테마 등은 모두 UI에 포함되고 다국어 배포를 위한 Translations 폴더 정도가 있는데 이 글에서는 따로 정리하지 않았다.

1. Data

  • Provider, Model, Repository 등이 포함되는 폴더이다.
  • 데이터와 관련된 파일들을 모아놓은 폴더 같다.

1-1. Provider

api.dart

import 'dart:convert';

import 'package:flutter_study/pages/getx_pattern/data/model/model.dart';
import 'package:http/http.dart' as http;

var baseUrl = Uri.parse('https://jsonplaceholder.typicode.com/posts/');

class MyApiClient {
  final http.Client httpClient;
  MyApiClient({required this.httpClient});

  getAll() async {
    try {
      var response = await httpClient.get(baseUrl);
      if (response.statusCode == 200) {
        Iterable jsonResponse = json.decode(response.body);
        List<MyModel> listMyModel =
            jsonResponse.map((model) => MyModel.fromJson(model)).toList();
        return listMyModel;
      } else
        print('erro');
    } catch (_) {}
  }
}
  • Provider는 api 통신 혹은 DB에 접근하는 폴더이다.
    (Provider 상태관리에서 Provider랑은 조금 다른 의미로 사용하는 것 같다)
  • 공식 사이트에서는 단일 파일로 관리해도 되지만 요청이 많을 경우에는 Entity 별로 분류해서 작성하는 것도 선택할 수 있다고 한다.

1-2. Model

model.dart

class MyModel {
  late int id;
  late String title;
  late String body;

  MyModel({
    id,
    title,
    body,
  });

  MyModel.fromJson(Map<String, dynamic> json) {
    id = json['id'];
    title = json['title'];
    body = json['body'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['name'] = this.title;
    data['body'] = this.body;
    return data;
  }
}
  • 모델 폴더이다. 기존에 사용하던 모델과 크게 차이가 없는 것 같다
  • 공식사이트에서는 Model이랑 Class 중에 편한걸 사용하라고 하는데 아마 Model을 사용 할 것 같다.
  • API 작업을 할 때 json 변환 메서드를 포함한다.
    (freezed 사용하기 딱 좋을 것 같다!!!!)

1-3. Repository

home_repository.dart

import 'package:flutter_study/pages/getx_pattern/data/provider/api.dart';

class MyRepository {
  final MyApiClient apiClient;

  MyRepository({required this.apiClient}) : assert(apiClient != null);

  getAll() {
    return apiClient.getAll();
  }
}
  • Repositoy 폴더는 Entity를 분리하는 폴더이다. 예시 코드에서는 그대로 반환하도록만 작성했다.
    (일반적으로 DB의 테이블 모델 그대로 가져와서 사용하는 것은 좋지 않다!)
  • Controller와 Data 사이에 연결을 한다고 생각하면 될 것 같다
    (이렇게 보니 MVC 패턴이랑 상당히 비슷해 보인다...)

2. Controller

home_controller.dart

import 'package:flutter_study/pages/getx_pattern/data/model/model.dart';
import 'package:flutter_study/pages/getx_pattern/data/repository/repository.dart';
import 'package:flutter_study/pages/getx_pattern/route/app_pages.dart';
import 'package:get/get.dart';

class HomeController extends GetxController {
  final MyRepository repository;
  HomeController({required this.repository}) : assert(repository != null);

  final _postsList = <MyModel>[].obs;
  
  final _post = MyModel().obs;
  
  get postList => this._postsList.value;
  
  set postList(value) => this._postsList.value = value;

  get post => this._post.value;
  
  set post(value) => this._post.value = value;

  getAll() {
    repository.getAll().then((data) {
      this.postList = data;
    });
  }
}
  • GetxController를 포함하는 폴더이다.
  • 생성자를 통해 Repository를 불러와서 사용만 하면 된다.
    (어디서 많이 본듯한 패턴, test 할 때 편하다!)
  • Repository를 이용해 data를 이용하며, 다음과 같이 몇가지 규칙을 권장한다.

1. 모든 Contoller에는 하나의 Repository가 있어야 하며, 초기화에 필요하다.
(번역을 해서 문구가 어색하긴 한데 생성자를 통한 주입을 말하는 것 같다)

2. 각 page에 대해 하나 이상의 Controller를 사용하는 것이 좋다.

3. 모든 페이지가 단일 데이터를 사용할 경우, 같은 Controller로 여러 페이지 사용이 가능하다.

결국 데이터와 화면을 연결해줌으로써 각 데이터(Repositroy)마다 분리해서 사용하는 것을 권장하고 있다. Entity마다 최소 1개 이상의 Controller를 만들어서 사용을 해야 할 것 같다.

3. Binding

home_binding.dart

import 'package:flutter_study/pages/getx_pattern/controller/home_controller.dart';
import 'package:flutter_study/pages/getx_pattern/data/provider/api.dart';
import 'package:flutter_study/pages/getx_pattern/data/repository/repository.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;

class HomeBinding implements Bindings {
  
  void dependencies() {
    Get.lazyPut<HomeController>(() {
      return HomeController(
        repository: MyRepository(
          apiClient: MyApiClient(
            httpClient: http.Client(),
          ),
        ),
      );
    });
  }
}
  • 공식 사이트에선 UI에서 호출하지 않고 의존성 관리를 권장하고 있다. 각각 Repository와 API등을 초기화 하여 사용할 수 있다.
    (지금까지는 주로 Get.find를 사용해서 page에서 불렀는데, 규모가 커지면 이 방법이 훨씬 좋아보인다!)

4. UI

home_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_study/pages/getx_pattern/controller/home_controller.dart';
import 'package:get/get.dart';

class HomePage extends GetView<HomeController> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: GetX<HomeController>(initState: (state) {
          Get.find<HomeController>().getAll();
        }, builder: (_) {
          return _.postList.length < 1
              ? Center(child: CircularProgressIndicator())
              : ListView.builder(
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text(_.postList[index].title ?? 'a'),
                      subtitle: Text(_.postList[index].body ?? 'b'),
                      onTap: () => _.details(_.postList[index]),
                    );
                  },
                  itemCount: _.postList.length,
                );
        }),
      ),
    );
  }
}
  • 사용자에게 보여지는 화면을 구성하는 폴더이다

  • 공식 사이트에서는 각 페이지에 대한 디렉토리를 만들어서 해당 페이지 외에 위젯들을 각 폴더별로 구성하는 걸 권장하고 있다. (각 페이지 마다 폴더 및 위젯을 정리)
    ex) HomePage, DetailPage 따로 폴더 생성, 각각의 폴더에서 widget 폴더 따로 생성

  • github를 참고해보니 UI 폴더에는 각 플랫폼별 설정할 파일들이나 theme 관련 파일들도 모두 들어가는 것 같다.

5. ROUTE

route.dart

abstract class Routes{

  static const INITIAL = '/';
  static const DETAILS = '/details';
}
class AppPages {
  
  static final routes = [
    GetPage(name: Routes.INITIAL, page:()=> HomePage(),),
  ];
}
  • Route를 관리하는 폴더이다.
  • Get.to를 이용하는 방법과 Get.toNamed를 이용하는 방법이 있는데 예제 코드에서는 Get.toNamed를 이용하고 있다.
    (추상 클래스로 route name을 정의하고 page를 관리하는게 실수를 줄인다고 한다! 아직은 잘 이해되지 않는다...)

정리

간단하게 getx_pattern을 정리해보았다. 혼자 작업을 진행할 때는 상관이 없겠지만 같이 작업을 하는 경우엔 구조를 따라서 작성하는게 훨씬 편할 것 같다. 혼자서 하면 의존성 관리, 페이지 분리 신경 안쓰고 편한대로 막 작성했던 것 같다... 조금씩 구조에 익숙해지다보면 조금 더 나은 생산성을 기대할 수 있을 것 같다!

소스코드 https://github.com/leeeeeoy/flutter_personal_study/tree/master/lib/pages/getx_pattern


참고자료

profile
100년 후엔 풀스택

0개의 댓글