smart_dio
中文文档 | English
A plugin-based HTTP client built on Dio, designed for Flutter.
Features
- Plugin Architecture - Global plugins + per-request hot-swap, flexible composition of logging/caching/retry capabilities
- Type-Safe Errors - Dart 3 sealed class, 8 error types, exhaustive pattern matching
- Auto Decoder - Built-in
fromJson/fromBytes/fromResponsesupport for automatic response parsing - Multi-language Localization - Configurable error messages with built-in Chinese defaults, supports partial overrides
- Multi-Client Management - Support for multiple backends, canary environments, unified lifecycle management
Requirements
- Dart SDK:
>=3.0.0 <4.0.0 - Flutter:
>=3.10.0
Installation
dependencies:
smart_dio: ^1.1.0
Table of Contents
- Quick Start
- Core Concepts
- Configuration & Plugins
- Built-in Plugins
- Auto Decoder
- Error Handling
- Localization
- Multi-Client Management
- Advanced Usage
- FAQ
- License
Quick Start
import 'package:smart_dio/smart_dio.dart';
void main() async {
// 1. Initialize
SmartDio.init(SmartConfig(
options: BaseOptions(baseUrl: 'https://api.example.com'),
globalPlugins: [
LoggerPlugin(),
RetryPlugin(retries: 3),
],
));
// 2. Make request
try {
final response = await SmartDio.get<Map>('/users');
print(response.data);
} on SmartError catch (e) {
// 3. Type-safe error handling
switch (e) {
case NetworkError():
print('Network unavailable');
case TimeoutError():
print('Request timeout');
case ResponseError(:final statusCode):
print('Server error: $statusCode');
default:
print(e.message);
}
}
}
Core Concepts
| Concept | Description |
|---|---|
SmartConfig |
Global configuration containing BaseOptions and globalPlugins |
SmartPlugin |
Plugin base class defining onStart/onRequest/onResponse/onError/onFinish lifecycle |
SmartRequest<T> |
Request DTO with generic type and optional decoder support |
HttpMethod |
Type-safe enum for HTTP methods (get, post, put, delete, patch, head, options) |
SmartError |
Sealed error base class, 8 subtypes, supports exhaustive pattern matching |
SmartClientManager |
Multi-client manager supporting register/get/dispose named clients |
Comparison with Raw Dio
| Capability | Raw Dio | smart_dio |
|---|---|---|
| Plugin Composition | Manual Interceptor list management | globalPlugins + extraPlugins declarative config |
| Error Handling | DioExceptionType enum, easy to miss branches | sealed class + exhaustive matching, compile-time check |
| Retry/Cache/Logging | Separate interceptor configuration | Built-in plugins, ready to use |
| Multi-language Error Messages | No built-in support | Built-in Chinese defaults + configurable overrides |
| Multi-Client | Manual management of multiple Dio instances | SmartClientManager unified management |
Configuration & Plugins
SmartConfig
SmartDio.init(SmartConfig(
// Dio base configuration
options: BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 30),
headers: {'Accept': 'application/json'},
),
// Global plugin list
globalPlugins: [
LoggerPlugin(),
HeaderPlugin({'Authorization': () => 'Bearer $token'}),
ParamsPlugin({'platform': () => 'ios'}),
CachePlugin.memory(),
RetryPlugin(retries: 3),
],
// Error localizer (optional)
errorLocalizer: DefaultErrorLocalizer(),
));
Hot-Swap
// Add plugin for single request (hot-plug)
await SmartDio.get('/api',
extraPlugins: [CustomPlugin()],
);
// Disable plugin for single request (hot-unplug)
await SmartDio.get('/realtime',
excludePlugins: {CachePlugin},
);
// Hot-plug + hot-unplug
await SmartDio.post('/sensitive',
extraPlugins: [EncryptPlugin()],
excludePlugins: {LoggerPlugin},
);
Notes for interceptor-based plugins:
CachePlugin/RetryPlugininstall Dio interceptors on the client instance.excludePluginsonly skips smart_dio middleware hooks for the current request.- If an interceptor has already been installed on the Dio instance, it remains active for later requests on that client.
- For strict isolation, use a dedicated client key (via
SmartDio.register) or control behavior with per-request options.
Plugin Priority
Plugins execute in ascending order by priority:
| Plugin | Priority | Description |
|---|---|---|
| LoggerPlugin | 10 | Executes first, logs raw request |
| HeaderPlugin | 50 | Injects fixed headers |
| ParamsPlugin | 50 | Injects fixed parameters |
| CachePlugin | 50 | Cache handling |
| RetryPlugin | 80 | Retry logic |
Built-in Plugins
LoggerPlugin
LoggerPlugin(
// Custom Talker instance (optional)
talker: Talker(),
)
HeaderPlugin
// Static headers
HeaderPlugin({
'X-App-Version': '1.0.0',
})
// Dynamic headers (computed per request)
HeaderPlugin({
'Authorization': () => 'Bearer ${getToken()}',
'X-Timestamp': () => DateTime.now().millisecondsSinceEpoch.toString(),
})
ParamsPlugin
// GET requests: added to queryParameters
// POST requests: added to body data
ParamsPlugin({
'platform': () => Platform.isIOS ? 'ios' : 'android',
'version': () => packageInfo.version,
})
CachePlugin
// Memory cache
CachePlugin.memory()
// Custom cache configuration
CachePlugin(
options: CacheOptions(
store: MemCacheStore(),
policy: CachePolicy.request,
maxStale: Duration(days: 7),
),
)
For HTTP error fallback to cached data, configure hitCacheOnErrorCodes
or hitCacheOnNetworkFailure on CacheOptions:
CachePlugin(
options: CacheOptions(
store: MemCacheStore(),
policy: CachePolicy.refreshForceCache,
hitCacheOnErrorCodes: [404, 500],
hitCacheOnNetworkFailure: true,
),
)
Notes:
- Cache fallback only works after a cache entry already exists.
- With
CachePolicy.request, whether an entry exists depends on normal HTTP cacheability rules (for exampleCache-Control/ETagon the server response). If the response was not cacheable, an offline/network failure will still surface the original Dio error instead of replaying cache. - If you need offline replay even when the server response does not provide cache headers, prefer an explicit policy such as
CachePolicy.refreshForceCacheorCachePolicy.forceCache. hitCacheOnErrorCodesonly runs on Dio'sonErrorpath. IfvalidateStatustreats404as success, Dio will not enter the error path, so cached fallback will not trigger.- A business response like HTTP
200with body{ "code": "404" }is not an HTTP error fallback case; it will still be handled byResponseParserPluginas a business error. - Request-level
cacheOptionsis passed as a fullCacheOptionsobject for that request (not a field-by-field merge with global options). If you only want to tweak one field, derive from your global options withcopyWith(...).
RetryPlugin
// Basic usage
RetryPlugin(retries: 3)
// Custom retry logic (based on SmartError)
RetryPlugin(
retries: 3,
shouldRetry: (error) => switch (error) {
NetworkError() => true,
TimeoutError() => true,
ResponseError(:final statusCode) => statusCode >= 500,
_ => false,
},
)
// Use default retry logic
RetryPlugin(shouldRetry: RetryPlugin.defaultShouldRetry)
ResponseParserPlugin
Automatically parses unified backend response format (code/msg/data), throws BusinessError on business failure.
// Global configuration (default keys: code/msg/data)
ResponseParserPlugin(
successCodes: [0, '0'], // Success code list
)
// Custom key mapping
ResponseParserPlugin(
keyMap: ResponseKeyMap(
codeKey: 'status', // Default 'code'
msgKey: 'message', // Default 'msg'
dataKey: 'result', // Default 'data'
),
successCodes: [200, 'success'],
)
Auto Decoder
SmartRequest<T> supports automatic response decoding with three decoder types:
Decoder Types
| Type | Use Case | Input |
|---|---|---|
fromJson |
JSON response parsing | Parsed data (Map/List) or raw response |
fromBytes |
Binary protocols (Protobuf, etc.) | Uint8List |
fromResponse |
Full custom parsing | Raw Response object |
Priority
fromResponse > fromBytes (only for byte data) > fromJson > raw data
Usage Examples
// JSON decoding
final response = await SmartDio.request<User>(
SmartRequest<User>(
path: '/user',
method: HttpMethod.get,
fromJson: User.fromJson,
),
);
print(response.data?.name); // User object
// Protobuf decoding
final response = await SmartDio.request<UserProto>(
SmartRequest<UserProto>(
path: '/user',
method: HttpMethod.get,
fromBytes: UserProto.fromBuffer,
),
);
// Custom response parsing
final response = await SmartDio.request<User>(
SmartRequest<User>(
path: '/user',
fromResponse: (response) {
final json = response.data['payload']['user'];
return User.fromJson(json);
},
),
);
HttpMethod Enum
enum HttpMethod {
get('GET'),
post('POST'),
put('PUT'),
delete('DELETE'),
patch('PATCH'),
head('HEAD'),
options('OPTIONS');
}
// Usage
SmartRequest<User>(
path: '/users',
method: HttpMethod.post, // Type-safe
data: {'name': 'John'},
)
Error Handling
Decoder errors are wrapped as ParseError:
try {
final response = await SmartDio.request<User>(...);
} on ParseError catch (e) {
print('Source: ${e.source}'); // 'json', 'bytes', or 'response'
print('Expected: ${e.expectedType}'); // User
print('Raw: ${e.rawSnippet}'); // First 200 chars of raw data
}
Error Handling
SmartError Types
| Type | Description | Key Fields |
|---|---|---|
NetworkError |
Network unreachable | host, errorCode |
TimeoutError |
Timeout (connect/send/receive) | phase, timeout |
ResponseError |
HTTP error response | statusCode, statusMessage, responseData |
ParseError |
Data parsing failed | source, expectedType, rawSnippet |
BusinessError |
Business logic error | code, serverMessage, traceId |
CancelError |
Request cancelled | reason, cancelToken |
CertificateError |
SSL certificate error | host |
UnknownError |
Unknown error | originalError |
Pattern Matching
try {
final response = await SmartDio.get('/api');
} on SmartError catch (e) {
final message = switch (e) {
NetworkError() => 'Check your network connection',
TimeoutError(:final phase) => switch (phase) {
TimeoutPhase.connect => 'Connection timeout',
TimeoutPhase.send => 'Send timeout',
TimeoutPhase.receive => 'Receive timeout',
},
ResponseError(:final statusCode) when statusCode == 401 => 'Please login again',
ResponseError(:final statusCode) when statusCode == 404 => 'Resource not found',
ResponseError(:final statusCode) => 'Server error ($statusCode)',
BusinessError(:final code, :final serverMessage) => '[$code] $serverMessage',
CancelError() => 'Request cancelled',
ParseError() => 'Data parsing failed',
CertificateError() => 'Certificate verification failed',
UnknownError(:final originalError) => 'Unknown error: $originalError',
};
showToast(message);
}
Localization
Default Chinese Messages
SmartDio.init(SmartConfig(
errorLocalizer: DefaultErrorLocalizer(),
));
final message = config.errorLocalizer.localize(error);
Customize Default Localizer Messages Directly
SmartDio.init(SmartConfig(
errorLocalizer: const DefaultErrorLocalizer(
networkMessage: 'Network error',
connectTimeoutMessage: 'Connection timeout',
sendTimeoutMessage: 'Send timeout',
receiveTimeoutMessage: 'Receive timeout',
responseMessagePrefix: 'Server error',
parseMessage: 'Parse failed',
cancelMessage: 'Request canceled',
certificateMessage: 'Certificate invalid',
unknownMessage: 'Unknown error',
// Optional: if omitted, keeps using BusinessError.serverMessage
businessMessage: 'Business failed',
),
));
Custom Messages
final localizer = ConfigurableErrorLocalizer(
overrides: {
ErrorKey.network: (_, __) => 'Please check your network connection',
ErrorKey.response: template('Server error: {statusCode}'),
ErrorKey.timeout(TimeoutPhase.connect): (_, __) => 'Connection timed out',
},
);
Three-Layer Fallback
1. overrides configuration
↓ (not found)
2. fallback localizer
↓ (not set)
3. Default Chinese messages
Priority Rules (Important)
The final message returned by SmartResponse.errorMessage follows this order:
1. Request-level errorMessageBuilder (highest)
↓ (continues only when it returns null)
2. SmartConfig.errorLocalizer (e.g. ConfigurableErrorLocalizer)
↓ (when not configured)
3. DefaultErrorLocalizer (built-in Chinese defaults)
Note:
- In the SmartDio request pipeline, the resolved final message is written to both
response.errorMessageandresponse.error?.message. - If you manually call
SmartError.from(...)outside SmartDio requests, it still returns the raw default message from error mapping.
Multi-Client Management
Register Multiple Clients
// Register main API
SmartDio.init(SmartConfig(
options: BaseOptions(baseUrl: 'https://api.main.com'),
));
// Register other APIs
SmartDio.register('payment', SmartConfig(
options: BaseOptions(baseUrl: 'https://api.payment.com'),
globalPlugins: [LoggerPlugin(), EncryptPlugin()],
));
Use Specific Client
// Use default client
await SmartDio.get('/users');
// Use named client
await SmartDio.of('payment').doGet('/transactions');
Switch Default Client
SmartDio.setDefault('beta');
await SmartDio.get('/api'); // Uses beta client
Advanced Usage
Custom Plugin
class AuthPlugin extends SmartPlugin {
@override
int get priority => 40;
@override
String get name => 'AuthPlugin';
@override
Future<void> onRequest(RequestContext ctx, RequestOptions options) async {
final token = await TokenManager.getToken();
options.headers['Authorization'] = 'Bearer $token';
}
@override
Future<void> onError(RequestContext ctx, SmartError error) async {
if (error is ResponseError && error.statusCode == 401) {
await TokenManager.refreshToken();
}
}
}
Lifecycle Hooks
await SmartDio.get('/api',
onStart: (ctx) async => print('Request started'),
onSuccess: (ctx, response) async => print('Success: ${response.statusCode}'),
onError: (ctx, error) async => print('Error: $error'),
onFinish: (ctx, result) async {
switch (result) {
case FinishSuccess(response: final response):
print('Request finished with success: ${response.statusCode}');
case FinishError(error: final error):
print('Request finished with error: $error');
}
},
);
FAQ
Q: How to handle timeout?
// Global timeout
SmartDio.init(SmartConfig(
options: BaseOptions(
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 30),
),
));
// Per-request timeout
await SmartDio.get('/api',
optionsOverride: BaseOptions(receiveTimeout: Duration(minutes: 5)),
);
Q: How to cancel a request?
final cancelToken = CancelToken();
SmartDio.get('/api', cancelToken: cancelToken);
cancelToken.cancel('User navigated away');
Q: Plugin execution order?
onStart (priority ascending)
↓
onRequest (priority ascending)
↓
[HTTP Request]
↓
onResponse (priority ascending) or onError (priority ascending)
↓
onFinish (priority ascending, always executes)
License
This project is licensed under the BSD 3-Clause License.
Acknowledgments
smart_dio is built on these excellent open-source projects:
- dio - Powerful Dart HTTP client
- dio_cache_interceptor - Dio cache interceptor
- dio_smart_retry - Smart retry interceptor
- talker_dio_logger - Elegant logger
Libraries
- bean/smart_dio_content_type
- http/core/dio_factory
- http/core/smart_client_manager
- http/core/smart_config
- http/error/configurable_localizer
- http/error/default_localizer
- http/error/error_key
- http/error/error_localizer
- http/error/message_template
- http/error/smart_error
- http/error/timeout_phase
- http/hooks/lifecycle_hook
- http/hooks/lifecycle_hooks
- http/pipeline/context
- http/pipeline/middleware
- http/pipeline/middleware_chain
- http/plugins/cache_plugin
- http/plugins/header_plugin
- http/plugins/logger_plugin
- http/plugins/params_plugin
- http/plugins/response_parser_plugin
- http/plugins/retry_plugin
- http/plugins/smart_plugin
- http/plugins/smart_retry_interceptor
- http/request/smart_request
- http/request/smart_response
- http/smart_dio
- smart_dio
- smart_dio 2.0 - Barrel Export
- utils/smart_dio_util