이 글은 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 폴더 정도가 있는데 이 글에서는 따로 정리하지 않았다.
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 (_) {}
}
}
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;
}
}
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();
}
}
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;
});
}
}
1. 모든 Contoller에는 하나의 Repository가 있어야 하며, 초기화에 필요하다.
(번역을 해서 문구가 어색하긴 한데 생성자를 통한 주입을 말하는 것 같다)
2. 각 page에 대해 하나 이상의 Controller를 사용하는 것이 좋다.
3. 모든 페이지가 단일 데이터를 사용할 경우, 같은 Controller로 여러 페이지 사용이 가능하다.
결국 데이터와 화면을 연결해줌으로써 각 데이터(Repositroy)마다 분리해서 사용하는 것을 권장하고 있다. Entity마다 최소 1개 이상의 Controller를 만들어서 사용을 해야 할 것 같다.
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(),
),
),
);
});
}
}
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 관련 파일들도 모두 들어가는 것 같다.
route.dart
abstract class Routes{
static const INITIAL = '/';
static const DETAILS = '/details';
}
class AppPages {
static final routes = [
GetPage(name: Routes.INITIAL, page:()=> HomePage(),),
];
}
간단하게 getx_pattern을 정리해보았다. 혼자 작업을 진행할 때는 상관이 없겠지만 같이 작업을 하는 경우엔 구조를 따라서 작성하는게 훨씬 편할 것 같다. 혼자서 하면 의존성 관리, 페이지 분리 신경 안쓰고 편한대로 막 작성했던 것 같다... 조금씩 구조에 익숙해지다보면 조금 더 나은 생산성을 기대할 수 있을 것 같다!
소스코드 https://github.com/leeeeeoy/flutter_personal_study/tree/master/lib/pages/getx_pattern
참고자료