smart_dio 1.1.1 copy "smart_dio: ^1.1.1" to clipboard
smart_dio: ^1.1.1 copied to clipboard

Dio wrapper with pluggable pipelines, sealed 8-case errors, multilayer localization fallback, and SmartClientManager for multiple named clients.

Pub License: BSD-3-Clause

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/fromResponse support 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 #

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/RetryPlugin install Dio interceptors on the client instance.
  • excludePlugins only 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 example Cache-Control/ETag on 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.refreshForceCache or CachePolicy.forceCache.
  • hitCacheOnErrorCodes only runs on Dio's onError path. If validateStatus treats 404 as success, Dio will not enter the error path, so cached fallback will not trigger.
  • A business response like HTTP 200 with body { "code": "404" } is not an HTTP error fallback case; it will still be handled by ResponseParserPlugin as a business error.
  • Request-level cacheOptions is passed as a full CacheOptions object 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 with copyWith(...).

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.errorMessage and response.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:

2
likes
130
points
172
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Dio wrapper with pluggable pipelines, sealed 8-case errors, multilayer localization fallback, and SmartClientManager for multiple named clients.

Homepage

License

BSD-3-Clause (license)

Dependencies

dio, dio_cache_interceptor, dio_smart_retry, flutter, talker_dio_logger

More

Packages that depend on smart_dio