Flutter _ Retrofit
Retrofit
Retrofit은 통신(networking) 기능을 사용하기 쉽게 만들어놓은 라이브러리이다.
REST 기반의 서비스를 통해 json, xml등의 구조인 데이터를 쉽게 가져오고 업로드할 수 있다.
Flutter에서 Retrofit 사용하기
Flutter에서 Retorfit을 사용하기 위해서는 6가지의 라이브러리가 필요하다
1. Retrofit
2. dio
3. json_annotaion
4. retrofit_generator
5. build_runner
6. json_serializable
위의 라이브러리를 하나하나 설명하자면 다음과 같다.
1. Retrofit
- Retrofit 라이브러리는 위에서 설명한 바와 같다.
(https://pub.dev/packages/retrofit)
2. dio
- dio는 다트를 위한 HTTP 클라이언트이다. (인터셉터나 글로벌 설정등을 지원해준다.)
안드로이드 개발자에게는 OkHttpClient와 같은 개념으로 생각하면 쉬울 것 같다.
(https://pub.dev/packages/dio)
3. json_annotaion
- json_serializable에서 json 직렬화 및 역직렬화를 위한 코드를 만드는 데 사용하는 annotaion을 정의해준다.
즉, json_serializable에 사용할 annotaion이 들어있는 것이라 생각하면 쉽다.
(annotaion이란 메타데이터라 생각하면 쉽다.(데이터를 위한 데이터))
(https://pub.dev/packages/json_annotation)
4. retrofit_generator
- 말 그대로 retrofit을 생성해주는 애다. 이렇게 설명하면 감이 안 잡힐테니 아래의 예제를 보고 이해하자.
(https://pub.dev/packages/retrofit_generator)
5. build_runner
- build_runner는 pub외의 dart 코드를 사용해 파일을 생성해준다.
(https://pub.dev/packages/build_runner)
6. json_serializable
- json_serializable은 json핸들링을 위한 빌더를 제공해준다.
모델을 쉽게 생성해주는 애라고 생각하면 된다.
(https://pub.dev/packages/json_serializable)
이제 예를 들어 생각해보자.
서버를 가정해보자.
서버는 GET형식에 Body에 weight과 height인자를 받고, 응답으로 ObesityRate(double), BMI(int), contents(String)라는 형식으로 답한다.(요청 주소는 '서버주소/obesity')
모바일은 클라이언트의 입장이므로 어떻게 요청할지 생각해보자.
request와 response에 사용할 모델이 필요하고, 해당 모델들을 사용해 http 통신을 할 클라이언트가 필요하다.
이를 코드로 작성하면 아래와 같다.
import 'package:json_annotation/json_annotation.dart';
part 'RequestDto.g.dart';
@JsonSerializable()
class RequestDto{
@JsonKey(name: "weight")
double weight;
@JsonKey(name: "height")
double height;
RequestDto(this.weight, this.rawData);
factory RequestDto.fromJson(Map<String, dynamic> json) => _$RequestDtoFromJson(json);
Map<String, dynamic> toJson() => _$RequestDtoToJson(this);
}
위의 코드는 reuqest에 사용할 모델이다.
우선 코드에도 있듯이 @JsonSerializable()이 클래스의 상단에 있다. 위의 라이브러리 설명에 적혀있었듯이 retrofit에 사용할 모델을 만들기 위한 annotation이다. 그리고 아래 @JsonKey로 모델에 사용할 데이터 틀을 정의해준다.
그러면 part 'RequestDto.g.dart'란 무엇일까?
우선 part는 하나의 라이브러리를 여러 개의 파일로 분할할 수 있으며, 파일 내의 모든 코드에대해 접근이 가능하게 만든다고 한다.
즉, RequestDto의 모든 private 멤버들에 접근할 수 있게 해주는 것이다.(*.g.dart 는 정해진 형식)
이제 아래의 부분을 보자.
factory RequestDto.fromJson(Map<String, dynamic> json) => _$RequestDtoFromJson(json);
- factory - map에서 새로운 RequestDto 인스턴스를 생성하기 위한 팩토리 생성자
- 생성된 $RequestDtoFromJson() 생성자에게 map을 전달
즉, 모델 인스턴스를 생성하기 위한 팩토리이다.
Map<String, dynamic> toJson() => _$RequestDtoToJson(this);
- toJson은 json인코딩 지원을 선언하는 규칙이다.
- 이를 위해 생성된 private 헬퍼 메서드인 _$RequestDtoToJson()을 호출한다.
즉, 모델을 json으로 인코딩할 때 사용되는 규칙이다.
따라서 우리는
factory RequestDto.fromJson(Map<string, dynamic=""> json) => _$RequestDtoFromJson(json);
Map<String, dynamic> toJson() => _$RequestDtoToJson(this);
을 구현해야하게 된다.
하지만 여기서 build_runner 가 빛을 발할 때가 된다.
terminal에서 flutter run build_runner build 를 치면 위의 코드에 맞게 알아서 파일이 생성된다.
구현할 필요가 없어진다.(part 부분을 꼭 붙여야지 build_runner가 인식한다.)
정리하자면 모델 클래스에서는 @JsonSerializable로 모델 클래스라는 것을 정의하고, @JsonKey로 멤버들을 정의하고, factory로 모델 인스턴스 생성을 처리하고, toJson으로 json 인코딩을 처리한다.
이제 Response용 모델을 만들어보자
part 'ResponseDto.g.dart';
@JsonSerialazable
class ResponseDto{
@JsonKey(name:"ObesityRate")
double ObesityRate;
@JsonKey(name:"BMI")
int BMI;
@JsonKey(name"contents")
String contents;
ResponseDto(this.ObesitryRate, this.BMI, this.contents)
factory ResponseDto.fromJson(Map<String, dynamic> json) => _$ResponseDtoFromJson(json);
Map<String, dynamic> toJson() => _$ResponseDtoToJson(this);
}
response 모델도 request에서 사용할 모델과 같은 원리로 만들어진다.
이제 client를 구현해보자.
part 'APIClient.g.dart';
@RestApi(baseUrl : 서버 주소)
abstract class APIClient{
factory APIClient(Dio dio, {String baseUrl}) = _APIClient;
@GET("obesity")
Future<ResponseDto> requestAPI(@Body() RequestDto req);
}
@RestApi로 restapi에 사용할 클래스를 정의한다. 또한 factory로 APIClient 클래스 인스턴스를 처리해주고, @GET()형식으로 함수 하나를 정의한다. 해당 함수를 보면 알겠지만, 우리가 정의한 RequestDto와 ResponseDto를 사용해준다.
@Body부분은 가정할 때 서버에서 Body를 필요로 하기 때문에 @Body로 한 것이다. 만약 서버가 다른 형식의 인자를 원한다면 그 형식에 맞게 바꿔주면 된다.
해당 클래스를 보면 왜 추상클래스로 되어있을 까? 왜냐하면 part 'APIClient.g.dart'로 함수들을 처리할 것이기 때문이다.
위와 같이 코드를 치고 다시 터미널에서 flutter pub run build_runner build 를 해보자.
그러면 각 함수들에 해당하는 코드들이 생겨난다.
(새로 생겨난 코드들을 입맛에 맞게 바꾸자. 굳이 바꿔야하지는 않는다.)
이제 이 클라이언트를 어떻게 써먹을지 봐보자.
Future<void> requestObesity() async{
final client = APIClient(Dio(BaseOptions(contentType:"application/json")));
await client.requestObesity(72.0, 174.0).then((value){
print('API RESULT : ${value});
})
}