Flutter

Flutter _ Retrofit

flutter 개발하자 2021. 5. 24. 22:06

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});
    })
}