Chopper
HTTP client generation library for Dart/Flutter. Automatically generates HTTP client from Retrofit-like API definitions using source_gen. Enables flexible customization through converter and interceptor support. Provides time-saving code generation.
GitHub Overview
lejard-h/chopper
Chopper is an http client generator using source_gen and inspired from Retrofit.
Topics
Star History
Library
Chopper
Overview
Chopper is a "Retrofit-style HTTP client generator" developed for Dart and Flutter, utilizing annotation-based code generation to create HTTP client libraries. Inspired by Retrofit (Java/Android), it leverages source_gen and build_runner to automatically generate type-safe HTTP clients while avoiding reflection. Built on top of Dart's HTTP package, it significantly simplifies REST API integration in Flutter apps through declarative API definitions, comprehensive request/response conversion, and interceptor functionality, enabling maintainable network layers.
Details
Chopper 2025 edition has established itself as a standard choice for modern HTTP communication in the Flutter ecosystem. Following Retrofit's annotation approach while optimized for Dart's constraints, it achieves high-performance operation without reflection. Features declarative API definition with @ChopperApi classes, HTTP method annotations like @GET/@POST, and parameter annotations like @Path/@Query/@Body for intuitive and readable API layers. Provides rich converters like JsonConverter and FormUrlEncodedConverter, interceptors like HttpLoggingInterceptor, and authentication features through Authenticator, meeting enterprise-level API integration requirements.
Key Features
- Annotation-Driven Development: High readability through Retrofit-style declarative API definitions
- Automatic Code Generation: Type-safe HTTP client implementation generation via source_gen
- Rich Converters: Support for diverse formats including JSON and Form URL Encoded
- Interceptor Functionality: Flexible handling of logging, authentication, and request modification
- Dart HTTP Foundation: Stable communication infrastructure based on standard HTTP package
- Build-Time Optimization: High runtime performance through reflection-free operation
Pros and Cons
Pros
- Low learning curve for developers familiar with Retrofit
- Declarative and maintainable API definitions through annotation-based approach
- Type safety and compile-time error detection through code generation
- High extensibility through rich interceptors and converters
- Lightweight and stable operation based on Dart's HTTP package
- Excellent integration with Flutter's build process
Cons
- Initial setup requires build_runner and chopper_generator configuration
- Increased initial build time due to code generation process
- Feature limitations compared to Dio-based high-feature HTTP clients (Retrofit.dart)
- Manual implementation required for complex response transformation processing
- Separate handling needed for non-HTTP communication like real-time (WebSocket)
- Complex code generation file management in large-scale projects
Reference Pages
Code Examples
Installation and Basic Setup
# pubspec.yaml
dependencies:
chopper: ^8.0.2
json_annotation: ^4.9.0
dev_dependencies:
chopper_generator: ^8.0.1
build_runner: ^2.4.9
json_serializable: ^6.8.0
# Verification in Flutter project
flutter pub get
flutter pub run build_runner build
Basic API Service Definition
// api_service.dart
import 'package:chopper/chopper.dart';
import 'package:json_annotation/json_annotation.dart';
// Specify generated files
part 'api_service.chopper.dart';
part 'api_service.g.dart';
// Data model
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
final DateTime createdAt;
User({
required this.id,
required this.name,
required this.email,
required this.createdAt,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
@JsonSerializable()
class CreateUserRequest {
final String name;
final String email;
CreateUserRequest({required this.name, required this.email});
factory CreateUserRequest.fromJson(Map<String, dynamic> json) =>
_$CreateUserRequestFromJson(json);
Map<String, dynamic> toJson() => _$CreateUserRequestToJson(this);
}
// API service definition
@ChopperApi(baseUrl: '/api/v1')
abstract class ApiService extends ChopperService {
// Factory constructor (returns generated class)
static ApiService create([ChopperClient? client]) => _$ApiService(client);
// Basic GET request
@Get(path: '/users')
Future<Response<List<User>>> getUsers();
// GET request with path parameters
@Get(path: '/users/{id}')
Future<Response<User>> getUserById(@Path('id') int userId);
// GET request with query parameters
@Get(path: '/users')
Future<Response<List<User>>> getUsersWithPagination(
@Query('page') int page,
@Query('limit') int limit,
@Query('sort') String? sort,
);
// POST request (JSON body)
@Post(path: '/users')
Future<Response<User>> createUser(@Body() CreateUserRequest request);
// PUT request (update)
@Put(path: '/users/{id}')
Future<Response<User>> updateUser(
@Path('id') int userId,
@Body() CreateUserRequest request,
);
// DELETE request
@Delete(path: '/users/{id}')
Future<Response<void>> deleteUser(@Path('id') int userId);
// Request with custom headers
@Get(path: '/users/profile')
@FactoryConverter(request: FormUrlEncodedConverter.requestFactory)
Future<Response<User>> getCurrentUser(
@Header('Authorization') String token,
);
// POST with form data
@Post(path: '/auth/login')
@FactoryConverter(request: FormUrlEncodedConverter.requestFactory)
Future<Response<Map<String, dynamic>>> login(
@Field('username') String username,
@Field('password') String password,
);
}
// After code generation, run the following command:
// flutter pub run build_runner build
ChopperClient Configuration and Usage
// chopper_client.dart
import 'package:chopper/chopper.dart';
import 'package:dio/dio.dart';
import 'package:logging/logging.dart';
import 'api_service.dart';
class ApiManager {
late ChopperClient _chopperClient;
late ApiService _apiService;
ApiManager() {
_initializeClient();
}
void _initializeClient() {
_chopperClient = ChopperClient(
// Base URL configuration
baseUrl: Uri.parse('https://api.example.com'),
// Register services to use
services: [
ApiService.create(),
],
// JSON conversion configuration
converter: JsonConverter(),
// Error handling conversion
errorConverter: JsonConverter(),
// Interceptor configuration
interceptors: [
// Logging interceptor
HttpLoggingInterceptor(),
// Custom authentication interceptor
_AuthInterceptor(),
// Curl format log output (for debugging)
CurlInterceptor(),
],
// Request/Response conversion
converter: const JsonConverter(),
);
_apiService = _chopperClient.getService<ApiService>();
}
ApiService get apiService => _apiService;
void dispose() {
_chopperClient.dispose();
}
}
// Custom authentication interceptor
class _AuthInterceptor implements RequestInterceptor, ResponseInterceptor {
String? _accessToken;
void setToken(String token) {
_accessToken = token;
}
@override
FutureOr<Request> onRequest(Request request) async {
if (_accessToken != null) {
return request.copyWith(
headers: {
...request.headers,
'Authorization': 'Bearer $_accessToken',
},
);
}
return request;
}
@override
FutureOr<Response> onResponse(Response response) async {
if (response.statusCode == 401) {
// Token expiration handling
print('Authentication error: Please update token');
await _refreshToken();
}
return response;
}
Future<void> _refreshToken() async {
// Token refresh logic
print('Executing token refresh process');
}
}
// Usage example
void main() async {
final apiManager = ApiManager();
final apiService = apiManager.apiService;
try {
// Get user list
final usersResponse = await apiService.getUsers();
if (usersResponse.isSuccessful) {
final users = usersResponse.body;
print('User list: ${users?.length} items');
users?.forEach((user) => print('${user.name} (${user.email})'));
}
// Get specific user
final userResponse = await apiService.getUserById(1);
if (userResponse.isSuccessful) {
final user = userResponse.body;
print('User details: ${user?.name}');
}
// Get users with pagination
final paginatedResponse = await apiService.getUsersWithPagination(
1, // page
10, // limit
'created_at', // sort
);
// Create new user
final createRequest = CreateUserRequest(
name: 'John Doe',
email: '[email protected]',
);
final createResponse = await apiService.createUser(createRequest);
if (createResponse.isSuccessful) {
final createdUser = createResponse.body;
print('User creation completed: ID=${createdUser?.id}');
}
} catch (e) {
print('API error: $e');
} finally {
apiManager.dispose();
}
}
Advanced Configuration and Customization (Authentication, Error Handling, etc.)
// advanced_chopper_setup.dart
import 'package:chopper/chopper.dart';
import 'dart:convert';
import 'dart:io';
// Custom error response
class ApiError {
final String message;
final int code;
final Map<String, dynamic>? details;
ApiError({
required this.message,
required this.code,
this.details,
});
factory ApiError.fromJson(Map<String, dynamic> json) {
return ApiError(
message: json['message'] ?? 'Unknown error',
code: json['code'] ?? 0,
details: json['details'],
);
}
}
// Custom error converter
class ErrorConverter implements Converter {
const ErrorConverter();
@override
Request convertRequest(Request request) => request;
@override
Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {
if (response.statusCode >= 400) {
try {
final jsonData = json.decode(response.body);
final error = ApiError.fromJson(jsonData);
throw ChopperHttpException(response, error.message);
} catch (e) {
throw ChopperHttpException(response, 'API Error: ${response.statusCode}');
}
}
return response;
}
}
// Automatic retry interceptor
class RetryInterceptor implements RequestInterceptor, ResponseInterceptor {
final int maxRetries;
final Duration initialDelay;
final List<int> retryStatusCodes;
const RetryInterceptor({
this.maxRetries = 3,
this.initialDelay = const Duration(seconds: 1),
this.retryStatusCodes = const [500, 502, 503, 504],
});
@override
FutureOr<Request> onRequest(Request request) {
return request.copyWith(
headers: {
...request.headers,
'X-Retry-Count': '0',
},
);
}
@override
FutureOr<Response> onResponse(Response response) async {
if (!retryStatusCodes.contains(response.statusCode)) {
return response;
}
final retryCountHeader = response.base.request?.headers['X-Retry-Count'];
final currentRetryCount = int.tryParse(retryCountHeader ?? '0') ?? 0;
if (currentRetryCount >= maxRetries) {
return response;
}
// Exponential backoff retry
await Future.delayed(initialDelay * (currentRetryCount + 1));
print('Retrying request (${currentRetryCount + 1}/$maxRetries): ${response.base.request?.url}');
// Update retry count for new request
final retryRequest = response.base.request?.copyWith(
headers: {
...response.base.request?.headers ?? {},
'X-Retry-Count': '${currentRetryCount + 1}',
},
);
if (retryRequest != null) {
return response.base.request?.client?.send(retryRequest) ?? response;
}
return response;
}
}
// Timeout configuration interceptor
class TimeoutInterceptor implements RequestInterceptor {
final Duration timeout;
const TimeoutInterceptor({this.timeout = const Duration(seconds: 30)});
@override
FutureOr<Request> onRequest(Request request) {
return request.copyWith(
headers: {
...request.headers,
'X-Request-Timeout': timeout.inMilliseconds.toString(),
},
);
}
}
// Advanced ChopperClient configuration
class AdvancedApiManager {
late ChopperClient _chopperClient;
late ApiService _apiService;
AdvancedApiManager() {
_initializeAdvancedClient();
}
void _initializeAdvancedClient() {
_chopperClient = ChopperClient(
baseUrl: Uri.parse('https://api.example.com'),
services: [
ApiService.create(),
],
// Multiple converter configuration
converter: JsonConverter(),
errorConverter: ErrorConverter(),
interceptors: [
// Timeout configuration
TimeoutInterceptor(timeout: Duration(seconds: 60)),
// Authentication interceptor
AuthInterceptor(),
// Retry interceptor
RetryInterceptor(
maxRetries: 5,
initialDelay: Duration(seconds: 2),
retryStatusCodes: [429, 500, 502, 503, 504],
),
// Response compression
(Request request) {
return request.copyWith(
headers: {
...request.headers,
'Accept-Encoding': 'gzip, deflate',
},
);
},
// Logging (conditional in production)
if (isDebugMode()) HttpLoggingInterceptor(),
],
);
_apiService = _chopperClient.getService<ApiService>();
}
ApiService get apiService => _apiService;
bool isDebugMode() {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
void dispose() {
_chopperClient.dispose();
}
}
// Authentication management class
class AuthInterceptor implements RequestInterceptor, ResponseInterceptor {
String? _accessToken;
String? _refreshToken;
DateTime? _tokenExpiry;
void setTokens(String accessToken, String refreshToken, DateTime expiry) {
_accessToken = accessToken;
_refreshToken = refreshToken;
_tokenExpiry = expiry;
}
void clearTokens() {
_accessToken = null;
_refreshToken = null;
_tokenExpiry = null;
}
@override
FutureOr<Request> onRequest(Request request) async {
// Token expiration check
if (_accessToken != null && _tokenExpiry != null) {
if (DateTime.now().isAfter(_tokenExpiry!.subtract(Duration(minutes: 5)))) {
await _refreshAccessToken();
}
}
if (_accessToken != null) {
return request.copyWith(
headers: {
...request.headers,
'Authorization': 'Bearer $_accessToken',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
}
return request;
}
@override
FutureOr<Response> onResponse(Response response) async {
if (response.statusCode == 401) {
// Access token expired
if (await _refreshAccessToken()) {
// Retry
final retryRequest = response.base.request?.copyWith(
headers: {
...response.base.request?.headers ?? {},
'Authorization': 'Bearer $_accessToken',
},
);
if (retryRequest != null) {
return response.base.request?.client?.send(retryRequest) ?? response;
}
} else {
// Refresh failed - logout process
clearTokens();
throw UnauthorizedException('Authentication failed. Please log in again.');
}
}
return response;
}
Future<bool> _refreshAccessToken() async {
if (_refreshToken == null) return false;
try {
// Update access token with refresh token
final response = await ChopperClient(
baseUrl: Uri.parse('https://api.example.com'),
converter: JsonConverter(),
).send(Request(
'POST',
Uri.parse('/auth/refresh'),
Uri.parse('https://api.example.com'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'refresh_token': _refreshToken}),
));
if (response.statusCode == 200) {
final data = json.decode(response.body);
_accessToken = data['access_token'];
_tokenExpiry = DateTime.now().add(Duration(seconds: data['expires_in']));
return true;
}
} catch (e) {
print('Token refresh error: $e');
}
return false;
}
}
// Custom exception classes
class UnauthorizedException implements Exception {
final String message;
UnauthorizedException(this.message);
@override
String toString() => 'UnauthorizedException: $message';
}
class ChopperHttpException implements Exception {
final Response response;
final String message;
ChopperHttpException(this.response, this.message);
@override
String toString() => 'ChopperHttpException: $message (${response.statusCode})';
}
Error Handling and Practical Usage Examples
// error_handling_example.dart
import 'package:chopper/chopper.dart';
import 'dart:io';
// Integrated error handling class
class ApiResponseHandler {
static Future<T?> handleResponse<T>(
Future<Response<T>> responseFuture, {
String? context,
bool showUserError = true,
}) async {
try {
final response = await responseFuture;
if (response.isSuccessful) {
return response.body;
} else {
await _handleErrorResponse(response, context, showUserError);
return null;
}
} on SocketException {
_showError('Network error: Please check your internet connection', showUserError);
return null;
} on TimeoutException {
_showError('Timeout: Please wait a moment and try again', showUserError);
return null;
} on ChopperHttpException catch (e) {
_showError('API Error: ${e.message}', showUserError);
return null;
} catch (e) {
_showError('An unexpected error occurred: $e', showUserError);
return null;
}
}
static Future<void> _handleErrorResponse(
Response response,
String? context,
bool showUserError,
) async {
String errorMessage = 'Unknown error';
switch (response.statusCode) {
case 400:
errorMessage = 'Bad request';
break;
case 401:
errorMessage = 'Authentication required';
// Execute automatic logout process, etc.
await _handleUnauthorized();
break;
case 403:
errorMessage = 'Access denied';
break;
case 404:
errorMessage = 'Requested resource not found';
break;
case 422:
// Detailed validation error analysis
errorMessage = await _parseValidationErrors(response);
break;
case 429:
errorMessage = 'Too many requests. Please wait and try again';
break;
case 500:
errorMessage = 'Server error occurred';
break;
case 502:
case 503:
case 504:
errorMessage = 'Server temporarily unavailable';
break;
default:
errorMessage = 'Error occurred (${response.statusCode})';
}
if (context != null) {
errorMessage = '$context: $errorMessage';
}
_showError(errorMessage, showUserError);
}
static Future<String> _parseValidationErrors(Response response) async {
try {
final errorData = json.decode(response.body);
if (errorData['errors'] != null) {
final errors = errorData['errors'] as Map<String, dynamic>;
final errorMessages = errors.values
.expand((error) => error is List ? error : [error])
.join(', ');
return 'Validation error: $errorMessages';
}
} catch (e) {
print('Error parsing validation errors: $e');
}
return 'Validation error occurred';
}
static Future<void> _handleUnauthorized() async {
// Token clearing, logout process
print('Authentication error: Executing logout process');
// Execute navigation to login screen, etc.
}
static void _showError(String message, bool show) {
if (show) {
print('Error: $message');
// In actual apps, display with Toast, Snackbar, Dialog, etc.
}
}
}
// Practical usage example
class UserRepository {
final ApiService _apiService;
UserRepository(this._apiService);
Future<List<User>?> getUsers({int page = 1, int limit = 20}) async {
return ApiResponseHandler.handleResponse(
_apiService.getUsersWithPagination(page, limit, 'created_at'),
context: 'Getting user list',
);
}
Future<User?> getUserById(int userId) async {
return ApiResponseHandler.handleResponse(
_apiService.getUserById(userId),
context: 'Getting user details',
);
}
Future<User?> createUser(String name, String email) async {
final request = CreateUserRequest(name: name, email: email);
return ApiResponseHandler.handleResponse(
_apiService.createUser(request),
context: 'Creating user',
);
}
Future<User?> updateUser(int userId, String name, String email) async {
final request = CreateUserRequest(name: name, email: email);
return ApiResponseHandler.handleResponse(
_apiService.updateUser(userId, request),
context: 'Updating user',
);
}
Future<bool> deleteUser(int userId) async {
final result = await ApiResponseHandler.handleResponse(
_apiService.deleteUser(userId),
context: 'Deleting user',
);
return result != null;
}
// Pagination-aware full retrieval
Future<List<User>> getAllUsers({int batchSize = 50}) async {
final allUsers = <User>[];
int currentPage = 1;
while (true) {
final users = await getUsers(page: currentPage, limit: batchSize);
if (users == null || users.isEmpty) {
break;
}
allUsers.addAll(users);
if (users.length < batchSize) {
// Reached last page
break;
}
currentPage++;
// Wait to reduce API load
await Future.delayed(Duration(milliseconds: 100));
}
return allUsers;
}
}
// Usage example in Flutter Widget
class UserListScreen extends StatefulWidget {
@override
_UserListScreenState createState() => _UserListScreenState();
}
class _UserListScreenState extends State<UserListScreen> {
late UserRepository _userRepository;
List<User> _users = [];
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
final apiManager = ApiManager();
_userRepository = UserRepository(apiManager.apiService);
_loadUsers();
}
Future<void> _loadUsers() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final users = await _userRepository.getUsers();
setState(() {
_users = users ?? [];
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _createUser() async {
final user = await _userRepository.createUser('New User', '[email protected]');
if (user != null) {
setState(() {
_users.add(user);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('User created')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User List')),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: _createUser,
child: Icon(Icons.add),
),
);
}
Widget _buildBody() {
if (_isLoading) {
return Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $_error'),
ElevatedButton(
onPressed: _loadUsers,
child: Text('Retry'),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _loadUsers,
child: ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteUser(user.id),
),
);
},
),
);
}
Future<void> _deleteUser(int userId) async {
final success = await _userRepository.deleteUser(userId);
if (success) {
setState(() {
_users.removeWhere((user) => user.id == userId);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('User deleted')),
);
}
}
}