HTTP in Flutter
Reference
본 포스트는 코드팩토리 - Flutter 중급 강의 (opens in a new tab)를 참조하여 작성되었습니다.
Dio
Dio는 Dart로 작성된 HTTP 클라이언트 라이브러리이다.
아래는 기본적인 Dio의 사용 방법이다.
void sendRequest() async {
Dio dio = Dio();
final rawString = "test@codefactory.ai:testtest";
// ID PW를 base 64로 인코딩
Codec<String, String> stringToBase64 = utf8.fuse(base64);
String token = stringToBase64.encode(rawString);
final resp = await dio.post( // GET, PUT, DELETE 등 다양한 메소드가 있음
"http://$ip/auth/login",
options: Options(
// header의 authorization에 Basic base64로 ID PW를 넣음
headers: {"authorization": 'Basic $token'}),
);
}
Dio Interceptor
Dio는 Single Ton으로 구현을 해놓고 Interceptor를 물려주면 Dio의 요청, 응답 등에 따라서 사용자가 Custom으로 작성한 코드를 실행시킬 수 있다.
Future<Dio> test() async {
final Dio dio = Dio();
dio.interceptors.add(CustomInterceptor());
return dio;
}
class CustomInterceptor extends Interceptor {
// 요청 할 때
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
}
// 응답 받을 때
@override
void onResponse(Response options, ResponseInterceptorHandler handler) {
}
// 에러가 났을 때
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
}
}
활용방안
- onRequest에서 매번 @Header annotation을 붙이지 않고 Interceptor에서 처리해줄 수 있다.
@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
debugPrint('[REQUEST] ${options.method} ${options.uri}');
if (options.headers['accessToken'] == 'true') {
options.headers.remove('accessToken');
final token = await storage.read(key: ACCESS_TOKEN_KEY);
options.headers.addAll({
'authorization': 'Bearer $token',
});
}
return super.onRequest(options, handler);
}
- onError : 401 에러가 발생했을 때 refresh token을 통해 access token을 재발급 받는다.
@override
void onError(DioError err, ErrorInterceptorHandler handler) async {
final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY);
if (refreshToken == null) {
return handler.reject(err);
}
final isStatus401 = err.response?.statusCode == 401;
if (isStatus401) {
final dio = Dio();
final resp = await dio.post(
'http://$ip/auth/token',
options: Options(
headers: {
'authorization': 'Bearer $refreshToken',
},
),
);
final accessToken = resp.data['accessToken'];
final options = err.requestOptions;
options.headers.addAll({'authorization': 'Bearer $accessToken'});
// secureStorage에 새로 발급 받은 accessToken을 저장
await storage.write(key: ACCESS_TOKEN_KEY, value: accessToken);
final response = await dio.fetch(options);
return handler.resolve(response);
}
return handler.reject(err);
}
Retrofit
Json Mapping부터 API 요청까지를 총괄해주는 라이브러리이다.
아래의 코드와 같이 선언해주고 Code-gen해주면 된다.
part 'restaurant_repository.g.dart';
@RestApi()
abstract class RestaurantRepository {
// http://$ip/restaurant
factory RestaurantRepository(Dio dio, {String baseUrl}) = _RestaurantRepository;
@GET('/{id}') // http://$ip/restaurant/{id}
// Dio에도 Headers가 있기 때문에 숨겨줘야 함
@Headers({
"authorization" : "Bearer JWTTOKEN"
})
Future<RestaurantDetailModel> getRestaurantModel(
// @Path('id') String sid, // 이렇게 하면 실제로 들어오는 키와 다르게 parameter를 지정할 수 있음
@Path() String id // 실제로 들어오는 json key가 id일 경우 자동으로 Mapping
);
}
- Post 요청 예시
part 'test_repository.g.dart';
@RestApi()
abstract class RestaurantRepository {
// http://$ip/restaurant
factory RestaurantRepository(Dio dio, {String baseUrl}) = _RestaurantRepository;
@POST("/update/{id}/{nick_name}")
@Headers({
'authorization' : 'Baerer JWTToken'
})
Future<RestaurantDetailModel> getRestaurantModel(
@Path("id") String id,
@Path("nick_name") String nickname,
@Body() User user
);
}
// User class를 Json 직렬화 / 역직렬화 모델로 쓴다
@JsonSerializable()
class User {
final String name;
@JsonKey(name: 'last_login_date', fromJson: _stringToDate, toJson: _dateToString)
final DateTime lastLoginDate;
@JsonKey(name: 'nick_name')
final String nickName;
User({
required this.name,
required this.lastLoginDate,
required this.nickName,
});
static String _dateToString(DateTime date) {
return DateTime.now().toString();
}
static DateTime _stringToDate(String date) {
return DateTime.parse(date);
}
}
Header
Retrofit을 사용하다보면 특정한 값을 Header에 담아서 보내고 싶을 때가 있다.
예를 들어서 아래와 같은 API에 요청을 한다고 가정할 때, Header의 authorization이라는 Key에 UserID를 전송해야한다.
UserApiController.java
@GetMapping("/me2")
public Optional<UserDto> me2(
HttpServletRequest httpServletRequest,
@RequestHeader(name = "authorization", required = false) String authorizationCookie) {
log.info("authrization me2: " + authorizationCookie);
var optionalUserDto = userRepository.findById(authorizationCookie);
return optionalUserDto;
}
그럴 때 @Header
Annotation을 파라미터 앞에 붙여서 Key를 지정해주고 해당 파라미터의 값이 value로 요청되게 된다.
@RestApi(baseUrl: "http://localhost:8080/api/user")
abstract class UserRepository {
factory UserRepository(Dio dio, {String baseUrl}) = _UserRepository;
@GET("/me2")
Future<HttpResponse<UserDto>> me2(@Header("authorization") userId);
}
JsonSerializable
Json Ignore
@JsonKey(includeFromJson: false, includeToJson: false)