우선 현재까지의 오늘 실습 까지 완료 후 폴더를 보면 아래 사진과 같다. 지난 시간에는 View 폴더 내에는 스플래시 스크린 파일을 생성하고, Utilities 폴더에는 날씨(이름)와 이미지 매핑을 위한 imageMap 과 날짜 및 시간 스트링 포맷을 위한 헬퍼함수 작성을 위해 utilites.dart 파일을 생성하였다. 또한 Resources 폴더에는 하드 코딩을 피하기 위해 이미지들의 파일을 const 변수에 담아준 image 다트 파일과 앱의 스타일을 정의하기 위한 colors.dart 그리고 앱에서 참조하는 url (날씨 외부 api 링크) 을 const 변수에 담아준 app_url.dart 파일을 생성하였다. Data 폴더에는 외부 사이트와 상호작용을 할 때 생기는 에러를 제어하는 app_exceptions.dart 파일을 정의하고,api_services.dart 파일을 통해 외부 api 에 데이터를 요청하고,response 를 리턴하는 함수들을 구현하였다. 이 과정에서 문제가 생기면 에러 처리를 진행한다.
Model 폴더는 데이터의 변수명을 짓는 곳이라고 표현하면 편하다. json 파일의 ~한 인덱스의 값을 변수에 담아줘서 쉽게 접근 가능하도록 한다.
오늘은 ViewModel 에서 진행하는 처리를 배울 것이다.
실습을 진행하다 생긴 궁금증이다. Repositry 와 Model 모두 View model 에게 가공한 데이터를 전달하는 기능을 담당한다라는 느낌을 받아서 이의 차이점이 궁금해졌다.
Chat GPT 에 물어본 결과 다음과 같은 답변을 받았다.
Model 폴더는 애플리케이션의 데이터 구조를 정의한다. 예를 들어 사용자, 주문, 제품과 같은 도메인 모델이나, API 응답을 위한 모델 등이 이에 해당한다. Repository 폴더는 애플리케이션에서 데이터를 가져오고 저장하는 방법을 정의한다. 데이터 소스(웹 서비스, 로컬 DB) 와의 인터페이스를 제공하는 것이다.
예를 들어, API 호출을 처리하고 응답을 모델 객체로 변환하는 코드, 로컬 데이터베이스에서 데이터를 조회하거나 저장하는 코드 등이 이에 해당한다.
이렇게 설명을 들어도 명확하게 이해가 되지 않는데 쉽게 말하면
=Model은 "무엇(What)"인 데이터의 구조와 관련된 것이고, Repository는 "어떻게(How)" 데이터에 접근하고 조작할지에 대한 것이다.
이러한 분리는 코드의 유지보수 및 확장을 용이하게 합니다. 예를 들어, 데이터 소스가 변경되어도 Model에는 영향을 미치지 않고, 데이터 구조가 변경되어도 Repository 로직에는 영향을 미치지 않습니다.
Repositry 폴더 내에 home_repositry.dart 파일을 생성한다.
HomeRepositry 클래스는 외부 날씨 정보 API로부터 데이터를 가져오는 메서드(hitApi)를 제공한다. 이 메서드는 비동기적으로(asyncd 사용 및 비동기 함수 처리의 결과를 담는 Future 사용) 데이터를 가져오며, 완료되면 데이터를 반환한다.
Data 폴더내의 api_services.dart 파일에서 ApiServices 클래스 내부에 getApi 메소드를 작성하였다. 이것은 url 을 인자로 입력하면 Api 를 get 하는 메소드이다. get 해서 response 에 넣어준다. response 의 타입은 var 로 지정하여 알아서 지정하게 한다.
import 'package:mvvm_weather_with_apis_getx/Data/Network/api_services.dart';
import 'package:mvvm_weather_with_apis_getx/Resources/AppURl/app_url.dart';
class HomeRepositry
{
// you can call homereapositry with hitAPi
static Future<dynamic> hitApi() async
{
// AppUrl.url : 날씨 정보를 위한 외부 api url
// await : 해당 처리 끝나는 것 기다림
// var : 타입 알아서 지정
var response = await ApiServices().getApi(AppUrl.url);
return response;
}
}
다음은 본격적으로 View Model 폴더에서 코드를 구현할 것이다.
ViewModel 폴더 내부에 Controller 폴더를 생성하고 그 아래에 home_controller.dart 파일을 생성한다.
HomeController 는 GetX 라이브러리를 사용하여 상태 관리를 수행하고 있다.
상태 관리는 애플리케이션의 UI가 데이터 변화에 반응하여 적절하게 업데이트되도록 하는 중요한 과정이다.
상태 관리: GetX는 반응형 상태 관리 및 간단한 상태 관리를 지원한다. 개발자는 간편하게 상태를 관리하고, UI를 해당 상태에 따라 자동으로 업데이트 할 수 있다.
라우팅 관리: GetX는 라우팅을 단순화하며, 전환 애니메이션, 다이얼로그, 바텀시트 등의 기능을 쉽게 구현할 수 있게 해준다.
국제화 및 지역화: GetX는 국제화 및 지역화를 위한 툴을 제공하여, 다양한 언어와 지역 설정을 지원하는 애플리케이션을 쉽게 개발할 수 있도록 도와준다.
성능: GetX는 성능을 최적화하기 위해 설계되었다. 이는 애플리케이션의 반응성과 효율성을 향상시키는 데 기여한다.
코드를 보며 주석으로 설명하겠다.
import 'dart:async';
import 'package:get/get.dart';
import 'package:mvvm_weather_with_apis_getx/Model/data_model.dart';
import 'package:mvvm_weather_with_apis_getx/Repository/home_repositry.dart';
import 'package:mvvm_weather_with_apis_getx/Resources/images/image_assets.dart';
import 'package:mvvm_weather_with_apis_getx/Utilities/utilities.dart';
import '../../View/Home/home_screen.dart';
// GetxController 를 상속받아 상태 관리를 수행한다.
class HomeController extends GetxController {
// Rx : GetX 패키지에서 제공하는 타입으로 반응형 프로그래밍 지원(Rx 를 사용하면 Rx 로 <감싸진 변수>들은 그 데이터가 변할 때마다 UI 가 자동으로 업데이트 되도록 할 수 있다.)
// ? : Null Safety 심볼인 ? 심볼은 Dart 언어에서 해당 변수가 null 값을 가질 수 있음을 나타낸다. null 을 허용한다면 무조건 ? 심벌을 붙여야 한다.
// 따라서 아래 문장은 String data 인 name 의 값이 변경될 때마다 관련된 UI가 자동으로 업데이트 된다.
Rx<DataModel?> model = Rx<DataModel?>(null);
// Hours 객체를 저장하는 반응형 변수
Rx<Hours?> hours = Rx<Hours?>(null);
// 현재 선택된 인덱스를 나타내는 반응형 정수 변수
// .obs 는 GetX 라이브러리를 사용할 때 사용하며, .obs 를 사용하면 어떤 데이터도 '반응형'으로 만들 수 있다.
Rx<int> currentIndex = 0.obs;
// 애니메이션 상태를 나타내는 반응형 부울 변수
RxBool animator = false.obs;
// 현재 인덱스 값을 반환하는 메서드
int getCurrentIndex() => currentIndex.value;
// 입력된 인덱스가 현재 인덱스와 동일한지 비교
bool compareIndex(int index) => index == currentIndex.value;
// 주어진 인덱스에 해당하는 시간을 24시간 형식으로 변환
// ! nullcheck.
String getHour(int index) =>
Utilities.formateTimeWithoutAmPm(
model.value!.days![0].hours![index].datetime.toString());
// 주어진 인덱스에 해당하는 날씨 조건에 맞는 이미지 경로를 반환
String getImage(int index) =>
Utilities().imageMap[model.value!.days![0].hours![index].conditions
.toString()] == null
// if imageMap 이 null 이면 nigtStarRain 이미지 get
? ImageAssets.nightStarRain
// null 아니라면
: Utilities().imageMap[model.value!.days![0].hours![index].conditions
.toString()]!;
// 현재 모델의 주소와 시간대를 문자열로 반환
String getAddress() =>
'${model.value!.address.toString()}, \n${model.value!.timezone
.toString()}';
// 현재 시간의 날씨 조건을 반환
String getCondition() => hours.value!.conditions.toString();
// 현재 시간의 온도를 반환
String getCurrentTime() => hours.value!.temp!.toInt().toString();
// 현재 시간의 체감 온도를 반환
String getFeelLike() => hours.value!.feelslike!.toString();
// 현재 시간의 구름 커버 비율을 반환
String getCloudOver() => hours.value!.cloudcover!.toInt().toString();
// 현재 시간의 풍속을 반환
String getIntSpeed() => hours.value!.windspeed!.toInt().toString();
// 현재 시간의 습도를 반환
String getHumidity() => hours.value!.humidity!.toInt().toString();
// API를 호출하여 날씨 데이터를 가져오고 상태를 업데이트하는 메서드
getData() {
HomeRepositry.hitApi().then((value) {
model.value = DataModel.fromJson(value);
for (int i = 0; i < model.value!.days![0].hours!.length; i++) {
if (Utilities.checkTime(
model.value!.days![0].hours![i].datetime.toString())) {
hours.value = model.value!.days![0].hours![i];
currentIndex.value = i;
break;
}
}
// 사용자를 홈 화면으로 이동
Get.to(const HomeScreen());
});
}
//주어진 인덱스에 따라 시간을 설정하고 애니메이션 상태를 업데이트
setHour(int index)
{
// 지연 후 애니메이션 값을 true로 설정
Timer(const Duration(milliseconds: 100), () => animator.value = true);
currentIndex.value = index;
hours.value = model.value!.days![0].hours![index];
Timer(const Duration(milliseconds: 100), () => animator.value = false);
}
}