패키지, 저장소 및 앱 카테고리 검색

패키지 상세v10.3.2

retrofit_generator

retrofit generator는 source_gen을 사용하여 생성된 dio 클라이언트로, Chopper과 Retrofit에서 영감을 받았습니다.

AndroidiOSmacOSWindowsLinux
30일 다운로드
55.1만
좋아요
201
Pub 점수
160

패키지 설치

flutter pub add retrofit_generator

패키지 개요

공개 정보를 바탕으로 생성한 선택 가이드입니다.

Retrofit_generator은 source_gen을 사용하여 Dio 기반의 HTTP 클라이언트를 생성하는 Flutter용 코드 생성 도구로, Retrofit과 Chopper에서 영감을 받았습니다. 주석이 달린 인터페이스로부터 타입 안전한 클라이언트 클래스를 생성함으로써 API 통합을 간소화하고, 반복 코드를 줄이며 유지보수성을 향상시킵니다. 이 패키지는 JSON 직렬화를 지원하며 Android, iOS, macOS, Windows, Linux 등 여러 플랫폼에서 작동합니다. 개발자는 주석을 사용해 엔드포인트를 정의하고, 생성기는 효율적이고 깔끔한 클라이언트 코드를 생성합니다. 구조적이고 확장 가능한 REST API 상호작용이 필요한 프로젝트에 적합합니다.

사용 사례

  • 타입 안전한 HTTP 클라이언트 생성
  • API 클라이언트 생성 자동화
  • 네트워크 계층에서 반복 코드 줄이기
  • HTTP 요청을 위한 Dio 통합
  • Flutter 앱에서 확장 가능한 REST API 구축하기

주요 기능

  • 주석 기반의 API 정의
  • source_gen를 통한 코드 생성
  • Dio HTTP 클라이언트와의 통합
  • JSON 직렬화 지원
  • 크로스 플랫폼 호환성

적합한 대상

  • Dio를 사용하는 Flutter 개발자
  • 유지보수 가능한 API 클라이언트가 필요한 팀
  • 복잡한 REST API를 가진 프로젝트
  • 수동 HTTP 코드를 줄이려는 개발자
  • 일관된 API 레이어 설계가 필요한 앱

고려 사항

  • 코드 생성을 위해 build_runner가 필요합니다
  • 애노테이션과 source_gen에 대한 이해가 필요합니다
  • 빌드 시간을 늘릴 수 있습니다
  • 정적 API 정의에 국한됨
  • 정확한 주석 사용에 의존

원본 README

연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.

Retrofit For Dart

retrofit retrofit_generator Pub Likes Testing Coverage Status

retrofit.dart is a type conversion dio client generator using source_gen and inspired by Chopper and Retrofit.

Usage

Generator

Add the generator to your dev dependencies

dependencies:
  retrofit: ^4.9.0
  logger: ^2.6.0  # for logging purpose
  json_annotation: ^4.9.0

dev_dependencies:
  retrofit_generator: ^10.0.1
  build_runner: ^2.6.0
  json_serializable: ^6.10.0
Define and Generate your API
import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';

part 'example.g.dart';

@RestApi(baseUrl: 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/')
abstract class RestClient {
  factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;

  @GET('/tasks')
  Future<List<Task>> getTasks();
}

@JsonSerializable()
class Task {
  const Task({this.id, this.name, this.avatar, this.createdAt});

  factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);

  final String? id;
  final String? name;
  final String? avatar;
  final String? createdAt;

  Map<String, dynamic> toJson() => _$TaskToJson(this);
}
Configuration

You can customize the code generation behavior by creating a build.yaml file in your project root:

targets:
  $default:
    builders:
      retrofit_generator:
        options:
          # Control whether to add '// dart format off/on' comments (default: true)
          format_output: true
          # Enable automatic response type casting (default: true)
          auto_cast_response: true
          # Generate empty request body for methods without parameters (default: false)
          empty_request_body: false
          # Enable useResult annotation for methods (default: false)
          use_result: false
format_output

By default, retrofit_generator wraps the generated code with // dart format off and // dart format on comments to preserve the formatting. If you're combining retrofit with other generators (like riverpod) and need more control over formatting, you can disable this:

targets:
  $default:
    builders:
      retrofit_generator:
        options:
          format_output: false

then run the generator

# dart
dart pub run build_runner build

# for watch mode (recommended during development)
dart pub run build_runner watch
Lean Builder Support (Experimental)

Retrofit now has experimental support for lean_builder, a faster build system for Dart. While lean_builder support is still under development, the infrastructure has been added for future use.

Important: lean_builder is an optional dependency and is NOT required to use retrofit_generator. It's only needed if you want to try the experimental lean_builder support.

To prepare for lean_builder support, add it to your dev_dependencies:

dev_dependencies:
  lean_builder: ^0.1.2  # Optional - only if you want to use lean_builder

Note: For now, please continue using build_runner as shown above. Full lean_builder integration will be available in a future release once lean_builder reaches stability.

Use it
import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
import 'package:retrofit_example/example.dart';

final logger = Logger();

void main(List<String> args) {
  final dio = Dio(); // Provide a dio instance
  dio.options.headers['Demo-Header'] = 'demo header'; // config your dio headers globally
  final client = RestClient(dio);

  client.getTasks().then((it) => logger.i(it));
}

More

Types
Types conversion

Before you use the type conversion, please make sure that a factory Task.fromJson(Map<String, dynamic> json) must be provided for each model class. json_serializable is recommended to be used as the serialization tool.

@GET('/tasks')
Future<List<Task>> getTasks();

@JsonSerializable()
class Task {
  const Task({required this.name});

  factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);

  final String name;
}

For enums, we rely on the toString() method to convert it to a string. Override the toString() method to return the value you want.

enum Status {
  pending,
  completed;

  @override
  String toString() => name;
}

@GET('/tasks/{status}')
Future<List<Task>> getTasksByStatus(@Path() Status status);
Using dart_mappable

You can use dart_mappable for type conversion by setting the parser to Parser.DartMappable:

@RestApi(
  baseUrl: 'https://api.example.com',
  parser: Parser.DartMappable,
)
abstract class ApiService {
  factory ApiService(Dio dio) = _ApiService;

  @GET('/tasks')
  Future<List<Task>> getTasks();
}

@MappableClass()
class Task with TaskMappable {
  const Task({this.id, this.name});

  final String? id;
  final String? name;
}

Don't forget to add the required dependencies:

dependencies:
  dart_mappable: ^4.2.0

dev_dependencies:
  dart_mappable_builder: ^4.2.0

And generate the code:

dart run build_runner build

For a complete example, see the example_dartmappable directory.

Typed extras

If you want to add static extra to all requests.

class MetaData extends TypedExtras {
  final String id;
  final String region;

  const MetaData({required this.id, required region});
}

@MetaData(
  id: '1234',
  region: 'ng',
)
@GET("/get")
Future<String> fetchData();

HTTP Methods

The HTTP methods in the below sample are supported.

  @GET('/tasks/{id}')
  Future<Task> getTask(@Path('id') String id);
  
  @GET('/demo')
  Future<String> queries(@Queries() Map<String, dynamic> queries);
  
  @GET('https://httpbin.org/get')
  Future<String> namedExample(
      @Query('apikey') String apiKey,
      @Query('scope') String scope,
      @Query('type') String type,
      @Query('from') int from,
  );
  
  @PATCH('/tasks/{id}')
  Future<Task> updateTaskPart(
    @Path() String id, 
    @Body() Map<String, dynamic> map,
  );
  
  @PUT('/tasks/{id}')
  Future<Task> updateTask(@Path() String id, @Body() Task task);
  
  @DELETE('/tasks/{id}')
  Future<void> deleteTask(@Path() String id);
  
  @POST('/tasks')
  Future<Task> createTask(@Body() Task task);
  
  @POST('http://httpbin.org/post')
  @MultiPart()
  Future<void> createNewTaskFromFile(@Part() File file);
  
  @POST('http://httpbin.org/post')
  @FormUrlEncoded()
  Future<String> postUrlEncodedFormData(@Field() String hello);
Runtime Content-Type for Multipart Uploads

Use @PartMap() to provide runtime metadata (like contentType and fileName) for multipart file uploads:

  @POST('/api/files')
  @MultiPart()
  Future<void> uploadFile({
    @Part(name: 'file') required File file,
    @PartMap() Map<String, dynamic>? metadata,
  });
  
  // Usage - Upload different file types to the same endpoint
  
  // Upload a JPEG image
  await client.uploadFile(
    file: File('/path/to/image.jpg'),
    metadata: {
      'file_contentType': 'image/jpeg',
      'file_fileName': 'photo.jpg',
    },
  );
  
  // Upload a PDF document
  await client.uploadFile(
    file: File('/path/to/document.pdf'),
    metadata: {
      'file_contentType': 'application/pdf',
      'file_fileName': 'report.pdf',
    },
  );

The @PartMap() annotation accepts a Map<String, dynamic> with keys in the format:

  • '<partName>_contentType' - Sets the content type for the part
  • '<partName>_fileName' - Sets the file name for the part

Fallback behavior:

  • Runtime values from @PartMap() override static values from @Part() annotation
  • If @PartMap() value is not provided, uses static value from @Part() annotation
  • If neither is provided:
    • fileName defaults to the file's actual name (extracted from file path)
    • contentType defaults to null (Dio will auto-detect based on file extension)
Dynamic Field Names for Multiple Files

Use @Part() with Map<String, File> to upload multiple files with dynamic field names:

  @POST('/api/files')
  @MultiPart()
  Future<void> uploadFiles(@Part() Map<String, File> files);
  
  // Usage - Upload multiple files with custom field names
  await client.uploadFiles({
    'image[0]': File('/path/to/photo1.jpg'),
    'image[1]': File('/path/to/photo2.jpg'),
    'document': File('/path/to/report.pdf'),
  });

This feature also supports:

  • Map<String, MultipartFile> - For files already wrapped in MultipartFile
  • Map<String, List<int>> - For raw byte data
  • Nullable maps: Map<String, File>?

Use cases:

  • Uploading arrays of files where each file needs a unique indexed name (e.g., image[0], image[1])
  • Uploading files to endpoints that require specific field names determined at runtime
  • Sending multiple files of different types in a single request
Get original HTTP response
  @GET('/tasks/{id}')
  Future<HttpResponse<Task>> getTask(@Path('id') String id);

  @GET('/tasks')
  Future<HttpResponse<List<Task>>> getTasks();
Streaming and Server-Sent Events (SSE)

Retrofit supports streaming responses using @DioResponseType(ResponseType.stream). When using this annotation, the return type must be either Stream<Uint8List> (for raw bytes) or Stream<String> (for text).

import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart' hide Headers;
import 'package:retrofit/retrofit.dart';

@RestApi(baseUrl: 'https://api.example.com')
abstract class RestClient {
  factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;

  // For binary data (images, files, etc.)
  @GET('/download/file')
  @DioResponseType(ResponseType.stream)
  Stream<Uint8List> downloadFile();

  // For text data (SSE, NDJSON, etc.)
  @GET('/events')
  @DioResponseType(ResponseType.stream)
  Stream<String> getServerSentEvents();
}

Note: When using Stream<String>, make sure to import dart:convert in your source file.

Server-Sent Events (SSE)

For SSE support, you can use the simple_sse package to parse the stream:

dependencies:
  simple_sse: ^2.0.0
import 'package:simple_sse/simple_sse.dart';

final client = RestClient(dio);

// Get the string stream and transform it to SSE events
final eventStream = client
    .getServerSentEvents()
    .transform(const LineSplitter())
    .transform(const SseEventTransformer());

await for (final event in eventStream) {
  print('Event: ${event.event}');
  print('Data: ${event.data}');
  print('ID: ${event.id}');
}
HTTP Header
  • Add a HTTP header from the parameter of the method
  @GET('/tasks')
  Future<Task> getTasks(@Header('Content-Type') String contentType);
  • Add static HTTP headers
  import 'package:dio/dio.dart' hide Headers;

  // ...
  
  @GET('/tasks')
  @Headers(<String, dynamic>{
    'Content-Type': 'application/json',
    'Custom-Header': 'Your header',
  })
  Future<Task> getTasks();
  • Add global HTTP headers to all requests in the API

You can define headers at the @RestApi level that will be automatically included in all requests:

  @RestApi(
    baseUrl: 'https://api.example.com',
    headers: {
      'User-Agent': 'MyApp/1.0.0',
      'X-Platform': 'mobile',
    },
  )
  abstract class ApiService {
    factory ApiService(Dio dio, {String? baseUrl}) = _ApiService;

    // This request will automatically include User-Agent and X-Platform headers
    @GET('/users')
    Future<List<User>> getUsers();

    // You can add method-specific headers that combine with global headers
    @GET('/profile')
    @Headers(<String, dynamic>{'Authorization': 'Bearer token'})
    Future<User> getProfile();

    // Method-level headers override global headers with the same key
    @GET('/settings')
    @Headers(<String, dynamic>{'X-Platform': 'web'})
    Future<Settings> getSettings();
  }

Note: Method-level headers (via @Headers or @Header parameter) will override global headers if they have the same key.

Error Handling

catchError(Object) can be used for capturing the exception and failed response. You can get the detailed response info from DioError.response.

client.getTask('2').then((it) {
  logger.i(it);
}).catchError((obj) {
  // non-200 error goes here.
  switch (obj.runtimeType) {
    case DioException:
      // Here's the sample to get the failed response error code and message
      final res = (obj as DioException).response;
      logger.e('Got error : ${res.statusCode} -> ${res.statusMessage}');
      break;
  default:
    break;
  }
});

Errors can also be caught and handled at the client level using CallAdapters. For example:

class ErrorAdapter<T> extends CallAdapter<Future<T>, Future<T>> {
  @override
  Future<T> adapt(Future<T> Function() call) {
    try {
      return call();
    } catch (exception) {
      // Handle the exception and throw whatever exception you want
      throw MyCustomException();
    }
  }
}

@RestApi(callAdapter: ErrorAdapter)
abstract class RestClient {
  factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;

  @GET('/user')
  Future<User> getUser();
}

If you need to handle errors individually per API method instead of at the client level:

@RestApi()
abstract class RestClient {
  factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;

  @GET('/user')
  @UseCallAdapter(ErrorAdapter)
  Future<User> getUser();
}
Relative API baseUrl

If you want to use a relative baseUrl value in the RestApi annotation of the RestClient, you need to specify a baseUrl in dio.options.baseUrl.

@RestApi(baseUrl: '/tasks')
abstract class RestClient {
  factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;

  @GET('{id}')
  Future<HttpResponse<Task>> getTask(@Path('id') String id);

  @GET('')
  Future<HttpResponse<List<Task>>> getTasks();
}

dio.options.baseUrl = 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1';
final client = RestClient(dio);
Call Adapter

This feature allows you to adapt the return type of a network call from one type to another.

For example: Future → Future<Result>

This feature provides flexibility in handling API responses, enabling better integration with custom response wrappers or error handling libraries.

The CallAdapter takes the original return type R and transforms it into a new type T. This is particularly useful when working with response wrappers like Either, Result, or ApiResponse.

Below is an example using a custom CallAdapter with a Result wrapper:

  class MyCallAdapter<T> extends CallAdapter<Future<T>, Future<Result<T>>> {
    @override
    Future<Result<T>> adapt(Future<T> Function() call) async {
      try {
        final response = await call();
        return Result<T>.ok(response);
      } catch (e) {
        return Result.err(e.toString());
      }
    }
  }

  @RestApi(callAdapter: MyCallAdapter)
  abstract class RestClient {
    factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;

    @GET('/')
    Future<Result<User>> getUser();
  }
Multiple endpoints support

If you want to use multiple endpoints to your RestClient, you should pass your base url when you initiate RestClient. Any value defined in RestApi will be ignored.

@RestApi(baseUrl: 'this url will be ignored if baseUrl is passed')
abstract class RestClient {
  factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;
}

final client = RestClient(dio, baseUrl: 'your base url');

If you want to use the base url from dio.option.baseUrl, which has lowest priority, please don't pass any parameter to RestApi annotation and RestClient's structure method.

Multithreading (Flutter only)

If you want to parse models on a separate thread, you can take advantage of the compute function, just like Dio does when converting String data responses into json objects.

For each model that you use you will need to define 2 top-level functions:

FutureOr<Task> deserializeTask(Map<String, dynamic> json);
FutureOr<dynamic> serializeTask(Task object);

If you want to handle lists of objects, either as return types or parameters, you should provide List counterparts:

FutureOr<List<Task>> deserializeTaskList(Map<String, dynamic> json);
FutureOr<dynamic> serializeTaskList(List<Task> objects);

Finally, make sure you set your @RestApi to use the Parser.FlutterCompute parser:

@RestApi(parser: Parser.FlutterCompute)

E.g.

@RestApi(
  baseUrl: 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/',
  parser: Parser.FlutterCompute,
)
abstract class RestClient {
  factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;

  @GET('/task')
  Future<Task> getTask();

  @GET('/tasks')
  Future<List<Task>> getTasks();

  @POST('/task')
  Future<void> updateTasks(Task task);

  @POST('/tasks')
  Future<void> updateTasks(List<Task> tasks);
}

Task deserializeTask(Map<String, dynamic> json) => Task.fromJson(json);

List<Task> deserializeTaskList(List<Map<String, dynamic>> json) =>
    json.map((e) => Task.fromJson(e)).toList();

Map<String, dynamic> serializeTask(Task object) => object.toJson();

List<Map<String, dynamic>> serializeTaskList(List<Task> objects) =>
    objects.map((e) => e.toJson()).toList();

N.B. Avoid using Map values, otherwise multiple background isolates will be spawned to perform the computation, which is extremely intensive for Dart.

abstract class RestClient {
  factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;

  // BAD
  @GET('/tasks')
  Future<Map<String, Task>> getTasks();

  @POST('/tasks')
  Future<void> updateTasks(Map<String, Task> tasks);

  // GOOD
  @GET('/tasks_names')
  Future<TaskNames> getTaskNames();

  @POST('/tasks_names')
  Future<void> updateTasks(TaskNames tasks);
}

TaskNames deserializeTaskNames(Map<String, dynamic> json) =>
    TaskNames.fromJson(json);

@JsonSerializable
class TaskNames {
  const TaskNames({required this.taskNames});

  final Map<String, Task> taskNames;

  factory TaskNames.fromJson(Map<String, dynamic> json) =>
      _$TaskNamesFromJson(json);
}
Hide generated files

For the project not to be confused with the files generated by the retrofit you can hide them.

Android studio

File -> Settings -> Editor -> File Types

Add "ignore files and folders"

*.g.dart

Videos

Credits

  • JetBrains. Thanks for providing the great IDE tools.

Contributors ✨

Thanks goes to these wonderful people:

Contributions of any kind welcome!

Activities

Alt

카테고리

의존성

의존성 목록 9 개

소스 저장소

이 패키지를 사용하는 프로젝트

순위