搜索插件、代码仓与 APP 分类

插件详情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 是一个用于 Flutter 的代码生成工具,它使用 source_gen 创建基于 Dio 的 HTTP 客户端,灵感来自 Retrofit 和 Chopper。它通过从带注解的接口生成类型安全的客户端类,简化了 API 集成,减少了样板代码并提高了可维护性。该包支持 JSON 序列化,并可在 Android、iOS、macOS、Windows 和 Linux 等多个平台上运行。开发者通过注解定义端点,生成器会生成高效且简洁的客户端代码。非常适合需要结构化、可扩展 REST API 交互的项目。

适合场景

  • 生成类型安全的 HTTP 客户端
  • 自动化 API 客户端创建
  • 减少网络层的样板代码
  • 与 Dio 集成以进行 HTTP 请求
  • 在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 项

源码仓库

使用此插件的项目

所在榜单