EP19. API response object mapping
API Response 형태는 json으로 되어있습니다. Flutter에서 json은 결국 string 타입으로 API Response를 활용하기 위해서는 Flutter의 Object로 변환/매핑하는 과정이 필요합니다.
ex) Response
{
"code": 10000,
"message": "Success",
"result": {
"fcmTokens": [
"token_1",
"token_2",
"token_3",
"token_4"
]
}
}
JSON
복사
1. 공통 API Response Format Class 구현
패키지 lib/network디렉토리 하위에 response_format.dart파일을 추가합니다.
/// API 응답 형식을 정의하는 클래스.
/// 제네릭 타입 T는 JSON으로부터 파싱될 객체 타입을 나타냅니다.
class ResponseFormat<T> {
/// 응답 코드.
final int code;
/// 응답 메시지.
final String message;
/// 응답 결과. 제네릭 타입 T 또는 null일 수 있습니다.
final T? result;
/// 생성자.
ResponseFormat({
required this.code,
required this.message,
this.result,
});
/// JSON으로부터 ResponseFormat 객체를 생성하는 팩토리 생성자.
/// [json]은 응답 JSON 객체이며, [fromJsonT]는 제네릭 타입 T를 파싱하는 함수입니다.
factory ResponseFormat.fromJson(Map<String, dynamic> json, T Function(Map<String, dynamic>) fromJsonT) {
T? parsedResult;
if (json['result'] != null) {
parsedResult = fromJsonT(json['result']);
} else {
parsedResult = null;
}
return ResponseFormat(
code: json['code'],
message: json['message'],
result: parsedResult,
);
}
}
/// JSON 응답에서 결과가 없는 경우를 나타내는 클래스.
class EmptyResult {
/// 생성자.
EmptyResult();
/// JSON으로부터 EmptyResult 객체를 생성하는 팩토리 생성자.
/// [json]은 응답 JSON 객체입니다.
factory EmptyResult.fromJson(Map<String, dynamic> json) {
return EmptyResult();
}
}
Dart
복사
2. APIClient 파싱 로직 구현
import 'dart:convert';
import 'package:http/http.dart' as http;
+import 'package:swm_flutter_package/network/response_format.dart';
import 'package:swm_flutter_package/services/auth_service.dart';
class ApiClient {
@@ -21,46 +22,47 @@ class ApiClient {
// GET 요청을 처리하는 메서드입니다.
// 지정된 endpoint로 GET 요청을 보내고 응답을 처리합니다.
- Future<String> get(String endpoint) async {
+ Future<ResponseFormat<T>> get<T>(String endpoint, T Function(Map<String, dynamic>) fromJsonT) async {
final headers = _getHeaders();
final response = await http.get(
Uri.parse('$baseUrl$endpoint'),
headers: headers,
);
- return _handleResponse(response);
+ return _handleResponse<T>(response, fromJsonT);
}
// POST 요청을 처리하는 메서드입니다.
// 지정된 endpoint로 POST 요청을 보내고 응답을 처리합니다.
- Future<String> post(String endpoint, Object? body) async {
+ Future<ResponseFormat<T>> post<T>(String endpoint, Object? body, T Function(Map<String, dynamic>) fromJsonT) async {
final headers = _getHeaders();
final response = await http.post(
Uri.parse('$baseUrl$endpoint'),
headers: headers,
body: jsonEncode(body),
);
- return _handleResponse(response);
+ return _handleResponse<T>(response, fromJsonT);
}
// DELETE 요청을 처리하는 메서드입니다.
// 지정된 endpoint로 DELETE 요청을 보내고 응답을 처리합니다.
- Future<String> delete(String endpoint, Object? body) async {
+ Future<ResponseFormat<T>> delete<T>(String endpoint, Object? body, T Function(Map<String, dynamic>) fromJsonT) async {
final headers = _getHeaders();
final response = await http.delete(
Uri.parse('$baseUrl$endpoint'),
headers: headers,
body: jsonEncode(body),
);
- return _handleResponse(response);
+ return _handleResponse<T>(response, fromJsonT);
}
// HTTP 응답을 처리하는 메서드입니다.
// 상태 코드가 200-299일 경우 응답 본문을 반환하고, 그렇지 않으면 예외를 발생시킵니다.
- String _handleResponse(http.Response response) {
+ ResponseFormat<T> _handleResponse<T>(http.Response response, T Function(Map<String, dynamic>) fromJsonT) {
if (response.statusCode >= 200 && response.statusCode < 300) {
- return response.body;
+ final jsonResponse = jsonDecode(response.body) as Map<String, dynamic>;
+ return ResponseFormat<T>.fromJson(jsonResponse, fromJsonT);
} else {
- throw Exception('Failed to load data: ${response.statusCode}');
+ return ResponseFormat(code: -999, message: "Parsing Error", result: null);
}
}
}
Diff
복사
•
fromJson 생성자를 파라미터로 전달받아 사용하도록 변경합니다.
•
전체 코드 첨부
3. APIClient를 사용하는 곳에서의 Result 타입 전달
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_package_sample/views/home/home_view_model.dart';
import 'package:provider/provider.dart';
import 'package:swm_flutter_package/network/api_client.dart';
+import 'package:swm_flutter_package/network/response_format.dart';
class HomeView extends StatelessWidget {
HomeView({super.key});
@@ -55,7 +56,7 @@ class HomeView extends StatelessWidget {
ElevatedButton(
onPressed: () {
// '추가' 버튼을 눌렀을 때 API의 POST 엔드포인트를 호출
- apiClient.post('/users/v1/fcmToken', {'fcmToken': textController.text})
+ apiClient.post<EmptyResult>('/users/v1/fcmToken', {'fcmToken': textController.text}, (json) => EmptyResult.fromJson(json))
.then((value) => print(value)) // 응답을 출력
.catchError((error) => print(error)); // 에러를 출력
},
@@ -65,7 +66,7 @@ class HomeView extends StatelessWidget {
ElevatedButton(
onPressed: () {
// '삭제' 버튼을 눌렀을 때 API의 DELETE 엔드포인트를 호출
- apiClient.delete('/users/v1/fcmToken', {'fcmToken': textController.text})
+ apiClient.delete<EmptyResult>('/users/v1/fcmToken', {'fcmToken': textController.text}, (json) => EmptyResult.fromJson(json))
.then((value) => print(value)) // 응답을 출력
.catchError((error) => print(error)); // 에러를 출력
},
@@ -75,8 +76,8 @@ class HomeView extends StatelessWidget {
ElevatedButton(
onPressed: () {
// '조회' 버튼을 눌렀을 때 API의 GET 엔드포인트를 호출
- apiClient.get('/users/v1/fcmToken')
- .then((value) => print(value)) // 응답을 출력
+ apiClient.get<FCMTokensResult>('/users/v1/fcmToken', (json) => FCMTokensResult.fromJson(json))
+ .then((value) => print(value.result?.tokens)) // 응답을 출력
.catchError((error) => print(error)); // 에러를 출력
},
child: Text('조회'),
@@ -88,4 +89,14 @@ class HomeView extends StatelessWidget {
),
);
}
+}
+
+class FCMTokensResult {
+ final List<String> tokens;
+
+ FCMTokensResult({required this.tokens});
+
+ factory FCMTokensResult.fromJson(Map<String, dynamic> json) {
+ return FCMTokensResult(tokens: List<String>.from(json['fcmTokens']));
+ }
}
Diff
복사
•
APIClient를 활용해 request를 만들때 Result타입을 제네릭으로 전달합니다.
•
전달한 타입이 Response Result 타입으로 적용되어 반환됩니다.
•
전체 코드 첨부