Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,37 @@ client.getTask('2').then((it) {
});
```

#### Centralized Error Handling

Instead of adding `.catchError()` to every API call, you can provide a centralized error handler when creating the client:

```dart
void handleError(error) {
// Handle all API errors in one place
if (error is DioException) {
final res = error.response;
logger.e('API Error: ${res?.statusCode} -> ${res?.statusMessage}');
} else {
logger.e('Unexpected error: $error');
}
}

// Create client with error handler
final client = RestClient(dio, onError: handleError);

// All API calls will automatically use the error handler
client.getTask('2').then((it) {
logger.i(it);
});
// No need to add .catchError() - errors are automatically handled!
```

This approach:
- Reduces boilerplate code by eliminating repetitive `.catchError()` calls
- Provides a single place to handle errors for all API methods
- Works with all exceptions, including `DioException`, data conversion errors, and `SocketException`
- Can be shared across multiple API clients

### 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`.
Expand Down
14 changes: 14 additions & 0 deletions example/lib/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@ import 'package:retrofit_example/api_result.dart';

part 'example.g.dart';

/// Example REST API client
///
/// Usage with centralized error handling:
/// ```dart
/// void handleError(error) {
/// if (error is DioException) {
/// print('API Error: ${error.response?.statusCode}');
/// }
/// }
///
/// final client = RestClient(dio, onError: handleError);
/// // All API calls will automatically handle errors through handleError
/// ```
@RestApi(baseUrl: 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/')
abstract class RestClient {
factory RestClient(
Dio dio, {
String? baseUrl,
ParseErrorLogger? errorLogger,
Function? onError,
}) = RestClientYmlp;

@GET('/tasks/{id}')
Expand Down
105 changes: 86 additions & 19 deletions generator/lib/src/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {

static const _baseUrlVar = 'baseUrl';
static const _errorLoggerVar = 'errorLogger';
static const _onErrorVar = 'onError';
static const _queryParamsVar = 'queryParameters';
static const _optionsVar = '_options';
static const _localHeadersVar = '_headers';
Expand Down Expand Up @@ -150,6 +151,7 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
_buildDioField(),
_buildBaseUrlField(baseUrl),
_buildErrorLoggerFiled(),
_buildOnErrorField(),
])
..constructors.addAll(
annotateClassConsts.map(
Expand Down Expand Up @@ -202,6 +204,14 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
..modifier = FieldModifier.final$;
});

/// Builds the onError field.
Field _buildOnErrorField() => Field((m) {
m
..name = _onErrorVar
..type = refer('Function?')
..modifier = FieldModifier.final$;
});

/// Generates the constructor.
Constructor _generateConstructor(
String? url, {
Expand All @@ -227,6 +237,12 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
..name = _errorLoggerVar
..toThis = true,
),
Parameter(
(p) => p
..named = true
..name = _onErrorVar
..toThis = true,
),
]);
if (superClassConst != null) {
var superConstName = 'super';
Expand Down Expand Up @@ -375,7 +391,7 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
if (method.isAbstract) {
methods.add(_generateApiCallMethod(method, instantiatedCallAdapter)!);
}
if (callAdapter != null) {
if (callAdapter != null && instantiatedCallAdapter != null) {
methods.add(
_generateAdapterMethod(
method,
Expand All @@ -391,9 +407,18 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
/// Generates a method implementation wrapped by CallAdapter.
Method _generateAdapterMethod(
MethodElement2 m,
InterfaceType? callAdapter,
InterfaceType callAdapter,
String resultType,
) {
// Get the adapter class name - should never be null at this point
final adapterName = callAdapter.element3.name3;
if (adapterName == null) {
throw InvalidGenerationSourceError(
'CallAdapter class must have a valid name',
element: m,
);
}

return Method((methodBuilder) {
methodBuilder.returns = refer(
_displayString(m.returnType, withNullability: true),
Expand Down Expand Up @@ -422,11 +447,19 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
}
final args =
'${positionalArgs.map((e) => '$e,').join()} ${namedArgs.map((e) => '$e,').join()}';
methodBuilder.body = Code('''
return ${callAdapter?.element3.name3}<$resultType>().adapt(
() => _${m.displayName}($args),
);
''');
final adaptedCall = '$adapterName<$resultType>().adapt(() => _${m.displayName}($args))';

// Only wrap with catchError if onError exists and the adapted return type is a Future
final adaptedReturnType = callAdapter.superclass?.typeArguments.lastOrNull as InterfaceType?;
final shouldWrapWithCatchError = _isDartTypeFuture(adaptedReturnType);

if (shouldWrapWithCatchError) {
methodBuilder.body = Code('''
return $_onErrorVar != null ? $adaptedCall.catchError($_onErrorVar) : $adaptedCall;
''');
} else {
methodBuilder.body = Code('return $adaptedCall;');
}
});
}

Expand Down Expand Up @@ -832,6 +865,15 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
/// Checks if the return type is Future.
bool _isReturnTypeFuture(String type) => type.startsWith('Future<');

/// Checks if a DartType represents a Future.
/// More reliable than string matching as it uses the analyzer's type system.
bool _isDartTypeFuture(DartType? type) {
if (type == null) {
return false;
}
return type.isDartAsyncFuture;
}

/// Generates the HTTP request code block.
Code _generateRequest(
MethodElement2 m,
Expand All @@ -845,9 +887,7 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
final callAdapterOriginalReturnType =
callAdapter.superclass?.typeArguments.firstOrNull as InterfaceType?;
returnAsyncWrapper =
_isReturnTypeFuture(
callAdapterOriginalReturnType?.getDisplayString() ?? '',
)
_isDartTypeFuture(callAdapterOriginalReturnType)
? 'return'
: 'yield';
}
Expand Down Expand Up @@ -982,18 +1022,32 @@ class RetrofitGenerator extends GeneratorForAnnotation<retrofit.RestApi> {
: wrappedReturnType;
if (returnType == null || 'void' == returnType.toString()) {
if (isWrappedWithHttpResponseWrapper) {
blocks
..add(
if (returnAsyncWrapper == 'return') {
blocks.add(
refer(
'final $_resultVar = await $_dioVar.fetch',
).call([options], {}, [refer('void')]).statement,
)
..add(
);
blocks.add(
Code('''
final httpResponse = HttpResponse(null, $_resultVar);
$returnAsyncWrapper httpResponse;
return $_onErrorVar != null ? Future.value(httpResponse).catchError($_onErrorVar) : Future.value(httpResponse);
'''),
);
} else {
blocks
..add(
refer(
'final $_resultVar = await $_dioVar.fetch',
).call([options], {}, [refer('void')]).statement,
)
..add(
Code('''
final httpResponse = HttpResponse(null, $_resultVar);
$returnAsyncWrapper httpResponse;
'''),
);
}
} else {
blocks.add(
refer(
Expand Down Expand Up @@ -1419,14 +1473,27 @@ You should create a new class to encapsulate the response.
}
}
if (isWrappedWithHttpResponseWrapper) {
blocks.add(
Code('''
if (returnAsyncWrapper == 'return') {
blocks.add(
Code('''
final httpResponse = HttpResponse($_valueVar, $_resultVar);
return $_onErrorVar != null ? Future.value(httpResponse).catchError($_onErrorVar) : Future.value(httpResponse);
'''),
);
} else {
blocks.add(
Code('''
final httpResponse = HttpResponse($_valueVar, $_resultVar);
$returnAsyncWrapper httpResponse;
'''),
);
);
}
} else {
blocks.add(Code('$returnAsyncWrapper $_valueVar;'));
if (returnAsyncWrapper == 'return') {
blocks.add(Code('return $_onErrorVar != null ? Future.value($_valueVar).catchError($_onErrorVar) : Future.value($_valueVar);'));
} else {
blocks.add(Code('$returnAsyncWrapper $_valueVar;'));
}
}
}

Expand Down
Loading
Loading