Flutter
패키지
Flutter HTTP

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)