이 글은 공식 문서와 블로그 문서를 보고 정리한 글입니다.
Dio
공식 문서에는 A powerful Http client for Dart 라고 소개하고 있다. http 처럼 서버와 통신을 하기 위해 필요한 패키지다. 다른점이 있다면 사용하기 쉽게 보다 많은 기능들을 제공하고 있고 여러가지 커스텀을 해서 사용하기가 편한 것 같다. 공식 문서와 몇가지 예제를 참고하여 주요 기능들에 대해 정리해보았다.
요즈음 Riverpod와 Dio를 공부하고 있는데, 최근 새로운 프로젝트를 시작하면서 Dio, Retrofit, Json_Serializable 조합을 사용해보려고 하고있다. 따로따로 익힐땐 어렵지 않았는데 실제 여러가지 케이스에 대해 작성해보려고 하니 아직 이해가 많이 부족한 것 같다... 천천히 다시 공부해보면서 익혀보려고 한다.
pubspec.yaml
dependencies:
dio: ^4.0.0
dio 의존성만 추가해주면 된다. 현재 기준 4.0.0이 최신 버전이다. (Null Safety 적용)
var dio = Dio();
// 첫번째 방법
final response = await dio.get('/test?id=12&name=wendu');
// 두번째 방법
final response = await dio.get('/test', queryParameters: {'id': 12, 'name': 'wendu'});
// 세번째 방법
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': 'wendu'});
request와 response는 다음과 같이 요청 메소드와 url을 같이 작성해주면 된다. 각 메서드 별로 함께 넘길 수 있는 여러가지 파라미터들이 있다. 요청 방법에는 여러가지가 있는데 위의 예시처럼 쿼리로 넘길수도 있고 body를 통해 넘길수도 있다.
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를 통해 지정할 수 있다. 주로 사용하는 옵션들은 다음과 같다
옵션의 경우 각 요청마다 설정도 가능하고 처음 dio 객체를 생성할 때 설정도 가능하다. 공통적으로 사용되는 것은 dio 생성시에 설정하고, 그 외에 것들은 요청에 맞게 설정하는 식이 좋아보인다.
사실 최근 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가지 메서드가 있는데 각각 요청, 응답, 에러가 발생했을 때 동작을 처리할 수 있다. 공식 문서에 나와있는 몇가지 예제들을 정리해봤다.
인터셉터를 통해 요청을 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한다.
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를 반환하는 것이 그 예시이다. 실제 사용을 해보진 못했지만 에러가 났을 때 특정 경로로 재요청을 하는 식으로 사용이 가능할 것 같다.
// 기본 Log
dio.interceptors.add(LogInterceptor());
// CustomLog
dio.interceptors.add(CustomLogInterceptor());
class CustomLogInterceptor extends Interceptor {
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
super.onRequest(options, handler);
}
void onResponse(Response response, ResponseInterceptorHandler handler) {
print(
'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}',
);
super.onResponse(response, handler);
}
void onError(DioError err, ErrorInterceptorHandler handler) {
print(
'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
);
super.onError(err, handler);
}
}
Log 역시 지정할 수 있는데 dio에서 기본적으로 제공하는 로그를 사용할 수 있다. 개인적으로는 기본 로그 형태가 한눈에 보기 쉬운건 아니어서 위 예시처럼 직접 커스텀을 해서 사용할 것 같다. 커스텀은 위의 Interceptor 객체를 상속받아 각 메서드를 구현해주면 된다.
간단하게 dio와 retrofit을 이용해서 요청을 구현해보았다. 사용한 api는 다음과 같다
// https://reqres.in/api/users/2
{
"data": {
"id": 2,
"email": "janet.weaver@reqres.in",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
},
"support": {
"url": "https://reqres.in/#support-heading",
"text": "To keep ReqRes free, contributions towards server costs are appreciated!"
}
https://reqres.in/ 라는 곳의 API를 사용했다. 사실 이번에 정리하면서 처음 알게 되었는데 각 메서드 별로 작성되어 있어서 테스트를 할 때 종종 이용할 것 같다.
data.dart
part 'data.g.dart';
()
class User {
User({
required this.data,
});
Data data;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
()
class Data {
Data({
required this.id,
required this.email,
required this.firstName,
required this.lastName,
required this.avatar,
});
int id;
String email;
(name: 'first_name')
String firstName;
(name: 'last_name')
String lastName;
String avatar;
factory Data.fromJson(Map<String, dynamic> json) => _$DataFromJson(json);
Map<String, dynamic> toJson() => _$DataToJson(this);
}
먼저 사용할 모델을 정의해주었다. JsonSerializable을 이용해 json 메서드를 자동으로 생성해주도록 하였다.
rest_client.dart
import 'package:dio/dio.dart';
import 'package:flutter_study/pages/dio/data.dart';
import 'package:retrofit/retrofit.dart';
part 'rest_client.g.dart';
(baseUrl: 'https://reqres.in/api')
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
('/users/{id}')
Future<User> getUser({() required int id});
}
Retrofit을 이용해 사용할 RestClient를 생성해주었다. 예시에서 사용한 요청은 GET요청으로 id를 넘기면 User를 받도록 작성했다.
이렇게 두 파일을 작성하고 flutter pub run build_runner bulid를 실행해 코드를 생성해줬다.
dio_result_page.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_study/pages/dio/custom_log_interceptor.dart';
import 'package:flutter_study/pages/dio/data.dart';
import 'package:flutter_study/pages/dio/rest_client.dart';
// 테스트 API
// https://reqres.in/
class DioResultPage extends StatelessWidget {
DioResultPage({Key? key}) : super(key: key);
final dio = Dio()
..interceptors.add(
CustomLogInterceptor(),
);
Widget build(BuildContext context) {
final _client = RestClient(dio);
return Scaffold(
appBar: AppBar(
title: Text('Dio'),
),
body: Center(
child: FutureBuilder<User?>(
future: _client.getUser(id: 1),
builder: (context, snapshot) {
if (snapshot.hasData) {
User? userInfo = snapshot.data;
if (userInfo != null) {
Data userData = userInfo.data;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(userData.avatar),
SizedBox(height: 16.0),
Text(
'${userInfo.data.firstName} ${userInfo.data.lastName}',
style: TextStyle(fontSize: 24.0),
),
Text(
userData.email,
style: TextStyle(fontSize: 24.0),
),
],
);
}
}
return CircularProgressIndicator();
},
),
),
);
}
}
간단하게 FutureBuilder를 이용해 결과페이지를 작성해주었다. dio 생성시 interceptor를 추가했는데, 위에서 말한 CustomLog를 추가해주었다.
custom_log_interceptor.dart
import 'package:dio/dio.dart';
class CustomLogInterceptor extends Interceptor {
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
super.onRequest(options, handler);
}
void onResponse(Response response, ResponseInterceptorHandler handler) {
print(
'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}',
);
super.onResponse(response, handler);
}
void onError(DioError err, ErrorInterceptorHandler handler) {
print(
'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
);
super.onError(err, handler);
}
}
요청시에는 요청 메서드와 경로를, 응답시에는 응답 코드와 경로를, 에러가 발생했을 땐 에러와 경로를 출력하도록 작성했다.
결과 화면과 설정한 로그가 출력된 화면이다.
dio를 간단하게 정리해봤다. 사실 글에 적지 않았지만 파일 다운로드, FormData 요청, Cancellation, Transformer 등 여러가지 기능들이 있었는데 실제 자주 사용할 것 같은 기능들 위주로 간단하게 정리했다. 최근 프로젝트를 하면서 jwt 인증 토큰관련해서 코드를 작성하던 중 이해가 잘 되지 않아 작성이 조금 더뎠는데 참고해서 다시 차근차근 작성을 할 것 같다. 현재 retrofit을 같이 사용하고 있는데 retorifit으로 엔드포인트를 작성하고, dio에서 여러 옵션들을 설정해서 각 요청별 처리를 작성할 것 같다. 작성이 어느정도 진행되면 dio와 retrofit을 이용한 조금 더 복잡한 예제도 정리해서 올려야겠다.
소스코드 https://github.com/leeeeeoy/flutter_personal_study/tree/master/lib/pages/dio
참고자료