티스토리 뷰
1.Json Serialization(직렬화)
웹서버와 통신하여 데이터를 주고받을 때 사용
수동 직렬화
- dart의 jsonDeode()를 사용해서 디코드 할 수 있음.
- 모델 클래스 내에서 JSON 직렬화
class User {
final String name;
final String email;
User(this.name, this.email);
User.fromJson(Map<String, dynamic> json)
: name = json['name'],
email = json['email'];
Map<String, dynamic> toJson() => {
'name': name,
'email': email,
};
}
// 사용 할 때 디코딩
Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);
// 사용 할 때 인코딩
String json = jsonEncode(user);
코드 생성을 사용한 자동 직렬화
fromJson, ToJson(json 데이터 인코딩/디코딩을 위한 직렬화) 하기 위한 json_annotation, json_serializable 패키지을 추가함.
dependencies:
json_annotation: <version>
dev_dependencies:
build_runner: <version>
json_serializable: <version>
이후 @JsonSerializable을 사용하여 일반 클래스를 serializable 하게 만들어줌.
import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';
@JsonSerializable()
class Address {
String street;
String city;
Address(this.street, this.city);
factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
String firstName;
Address address;
User(this.firstName, this.address);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
> flutter pub run build_runner build 를 실행하면 user.g.dart, address.g.dart 파일이 생성됨.(이건 한번만 실행할 때)
> flutter pub run build_runner watch를 실행하면 똑같은 결과를 얻을 수 있고, 코드가 변경될 때마다 자동으로 실행된다.
결론적으로 작은 프로젝트나 테스트 프로젝트에서는 수동 직렬화를 사용하고, 조금 규모가 있는 프로젝트라면 fromJson, toJson을 만드는 과정에서 오타의 위험성이 존재하고 이것은 프로젝트 규모가 커질수록 관리가 더 힘들어지기 때문에 코드 생성을 통한 Serialize방식을 추천한다.
2. Retrofit
특징과 사용하는 이유
annotation 하나로 HTTP 엔드 포인트에 대한 액션들을 자동으로 클래스화 시켜주는 라이브러리.
엔드포인트 별로 메소드를 자동생성함. 다양한 언어와 프레임워크를 지원함.
엔드포인트 별 생성하고 싶은 메서드를 자동으로 생성해서 조금 더 편하게 개발 가능해서 사용함.
사용 방법
1. 패키지 추가(패키지 관리 링크)
- dependecies에 dio, retrofit과 json_annotation을 추가.
- dev_dependencies에 retrofit_generator, build_runner, json_serializable을 추가.
retrofit은 기본적으로 dio 기반으로 통신함. 그래서 dio와 retrofit, retrofit_generator, build_runner는 필수 요소
json 직렬화를 위한 패키지도 추가함. (=> json_annotation, json_serializable, build_runner)
2. 호출할 API 정의
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';
part 'RestClient.g.dart';
@RestApi(baseUrl: "https://xxx.xx.xx.xxx")
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
@POST("/example") // 해당 api 경로
Future<List<ListResponse>> getListData(
@Header("user_id") String userId,
@Header("Authorization") String token,
@Body() PracticeRequest practiceRequest);
}
@JsonSerializable()
class PracticeRequest {
int? id;
PracticeRequest({
required this.id
});
factory PracticeRequest.fromJson(Map<String, dynamic> json) => _$PracticeRequestFromJson(json);
Map<String, dynamic> toJson() => _$PracticeRequestToJson(this);
}
@JsonSerializable()
class ListResponse {
String? name;
int? age;
String? birth;
String? phone;
ListResponse({
required this.name,
required this.age,
required this.birth,
required this.phone
});
factory ListResponse.fromJson(Map<String, dynamic> json) => _$ListResponseFromJson(json);
Map<String, dynamic> toJson() => _$ListResponseToJson(this);
}
3. Generator 실행
flutter packages pub run build_runner build
위 커멘드를 실행하면 RestClient.g.dart 라는 파일이 아래와 같이 생성됨.
part of 'RestClient.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PracticeRequest _$PracticeRequestFromJson(Map<String, dynamic> json) {
return PracticeRequest(
id: json['id'] as int?,
);
}
Map<String, dynamic> _$PracticeRequestToJson(PracticeRequest instance) =>
<String, dynamic>{
'id': instance.id,
};
ListResponse _$ListResponseFromJson(Map<String, dynamic> json) {
return ListResponse(
name: json['name'] as String?,
age: json['age'] as int?,
birth: json['birth'] as String?,
phone: json['phone'] as String?,
);
}
Map<String, dynamic> _$ListResponseToJson(ListResponse instance) =>
<String, dynamic>{
'name': instance.name,
'age': instance.age,
'birth': instance.birth,
'phone': instance.phone,
};
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
class _RestClient implements RestClient {
_RestClient(this._dio, {this.baseUrl}) {
baseUrl ??= 'https://xxx.xx.xx.xxx';
}
final Dio _dio;
String? baseUrl;
@override
Future<List<ListResponse>> getListData(userId, token, practiceRequest) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(practiceRequest.toJson());
final _result = await _dio.fetch<List<dynamic>>(
_setStreamType<List<ListResponse>>(Options(
method: 'POST',
headers: <String, dynamic>{
r'user_id': userId,
r'Authorization': token
},
extra: _extra)
.compose(_dio.options, '/example',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
var value = _result.data!
.map((dynamic i) => ListResponse.fromJson(i as Map<String, dynamic>))
.toList();
return value;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
}
3.Dio
1. package 설정
pubspec.yaml
dependencies:
dio: <version>
dio 의존성만 추가해주면 된다. 현재 기준 4.0.0이 최신 버전이다. (Null Safety 적용)
2. request&response 방법
var dio = Dio();
// 첫번째 방법
final response = await dio.get('/test?id=12&name=xx');
// 두번째 방법
final response = await dio.get('/test', queryParameters: {'id': 12, 'name': 'xx'});
// 세번째 방법
final response = await dio.request(
'/test',
data: {'id':12,'name':'xx'},
options: Options(method:'GET'),
);
// post
final response = await dio.post('/test', data: {'id': 12, 'name': 'xx'});
3. options
var dio = Dio();
dio.options.baseUrl = 'https://www.xx.com/api';
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout = 3000;
var options = BaseOptions(
baseUrl: 'https://www.xx.com/api',
connectTimeout: 5000,
receiveTimeout: 3000,
);
Dio dio = Dio(options);
// BaseOptions 객체
BaseOptions({
String? method,
int? connectTimeout,
int? receiveTimeout,
int? sendTimeout,
String baseUrl = '',
Map<String, dynamic>? queryParameters,
Map<String, dynamic>? extra,
Map<String, dynamic>? headers,
ResponseType? responseType = ResponseType.json,
String? contentType,
ValidateStatus? validateStatus,
bool? receiveDataWhenStatusError,
bool? followRedirects,
int? maxRedirects,
RequestEncoder? requestEncoder,
ResponseDecoder? responseDecoder,
ListFormat? listFormat,
this.setRequestContentTypeWhenNoPayload = false,
})
dio 객체를 생성하면서 공통적으로 사용하고 싶은 것들을 BaseOptions를 통해 지정할 수 있다. 주로 사용하는 옵션들은 다음과 같다
- baseUrl: 요청할 기본 주소를 설정할 수 있다.
- connectTimeout: 서버로부터 응답받는 시간을 설정할 수 있다.
- receiveTimeout: 파일 다운로드 등과 같이 연결 지속 시간을 설정할 수 있다.
- headers: 요청의 header 데이터를 설정할 수 있다.
ex) 인증 토큰
4. Interceptor
사실 최근 dio를 공부하면서 가장 필요했던 부분이 아닌가 싶다. Interceptor는 요청때마다 가로채는 역할을 하는데, Interceptor를 통해 요청때마다 반복적인 작업을 처리할 수 있다. 예를 들어 토큰의 유효성을 검사한다거나 로그를 처리한다거나 등이 될 수 있다.
class Interceptor {
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) =>
handler.next(options);
void onResponse(
Response response,
ResponseInterceptorHandler handler,
) =>
handler.next(response);
void onError(
DioError err,
ErrorInterceptorHandler handler,
) =>
handler.next(err);
}
interceptor를 보면 3가지 메서드가 있는데 각각 요청, 응답, 에러가 발생했을 때 동작을 처리할 수 있다. 공식 문서에 나와있는 몇가지 예제들을 정리해봤다.
4-1. Lock/unlock the interceptors
인터셉터를 통해 요청을 lock/unlock 할 수 있다. 공식 문서에 따르면 이렇게 될 경우, 대기열에 추가되어 인터셉터가 unlock이 될 때까지 대기한다고 한다.
dio.interceptors.add(InterceptorsWrapper(
onRequest: (Options options, handler) async {
print('send request:path:${options.path},baseURL:${options.baseUrl}');
if (csrfToken == null) {
print('no token,request token firstly...');
//lock the dio.
dio.lock();
tokenDio.get('/token').then((d) {
options.headers['csrfToken'] = csrfToken = d.data['data']['token'];
print('request token succeed, value: ' + d.data['data']['token']);
print( 'continue to perform request:path:${options.path},baseURL:${options.path}');
handler.next(options);
}).catchError((error, stackTrace) {
handler.reject(error, true);
}) .whenComplete(() => dio.unlock()); // unlock the dio
} else {
options.headers['csrfToken'] = csrfToken;
handler.next(options);
}
}
));
예시 코드는 토큰의 유무를 검사하는 코드 예시이다. 인터셉터를 통해서 요청때마다 토큰의 유무를 검사하여 만약 토큰이 없다면 새로운 토큰을 요청해서 다시 이후에 요청을 진행한다. 이 때 토큰이 없다면 인터셉터를 lock해서 대기열에 넣은 후 토큰을 받고 나서 다시 unlock한다.
4-2. Resolve and reject the request
dio.interceptors.add(InterceptorsWrapper(
onRequest:(options, handler) {
return handler.resolve(Response(requestOptions:options,data:'fake data'));
},
));
Response response = await dio.get('/test');
print(response.data);
//'fake data'
모든 요청에 대해 응답을 제어할 수 있는데, 예시 코드처럼 모든 요청에 대해 fake data를 반환하는 것이 그 예시이다. 실제 사용을 해보진 못했지만 에러가 났을 때 특정 경로로 재요청을 하는 식으로 사용이 가능할 것 같다.
4-3. Log
// 기본 Log
dio.interceptors.add(LogInterceptor());
// CustomLog
dio.interceptors.add(CustomLogInterceptor());
class CustomLogInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
print(
'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}',
);
super.onResponse(response, handler);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
print(
'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
);
super.onError(err, handler);
}
}
Log 역시 지정할 수 있는데 dio에서 기본적으로 제공하는 로그를 사용할 수 있다. 개인적으로는 기본 로그 형태가 한눈에 보기 쉬운건 아니어서 위 예시처럼 직접 커스텀을 해서 사용할 것 같다. 커스텀은 위의 Interceptor 객체를 상속받아 각 메서드를 구현해주면 된다.