Как реализовать перехватчик повторных попыток в Flutter с Dio
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀
Давайте научимся реализовывать перехватчик повторных попыток в Flutter с использованием библиотеки Dio для повышения устойчивости сетевых запросов.
1. Что такое перехватчики?
В HTTP-клиенте перехватчик — это промежуточное ПО, которое находится между вашим приложением и сервером. Оно позволяет перехватывать и изменять исходящие HTTP-запросы и входящие ответы до того, как они достигнут кода вашего приложения. Это позволяет выполнять действия, такие как:
- Добавление заголовков
- Обработка ошибок
- Логирование запросов
- Выполнение проверок аутентификации
Перехватчики применяют эту логику последовательно ко всем вызовам API без необходимости дублировать код в каждом отдельном запросе.
2. Зачем использовать перехватчики?
Перехватчики действуют как промежуточное ПО, перехватывая HTTP-запросы и ответы на ключевых этапах их жизненного цикла. Архитектура HttpClient Interceptor предлагает несколько преимуществ:
- Централизованная логика: Инкапсуляция межсервисных задач, таких как аутентификация, обработка ошибок и логирование, в одном месте.
- Динамическое изменение запросов/ответов: Программное изменение запросов и ответов на основе состояния приложения или требований сервера.
- Глобальные стратегии восстановления после ошибок: Реализация последовательной обработки ошибок и механизмов повторных попыток по всему приложению.
Эти возможности особенно ценны для приложений, работающих с API, где сетевые операции должны обрабатывать:
- Аутентификацию на основе токенов
- Нестабильные условия сети
- Последовательное сообщение об ошибках
Разделяя сетевую логику и бизнес-логику, перехватчики соответствуют принципам SOLID, способствуя поддержке и масштабируемости кода.
3. Как работают перехватчики?
Перехватчики работают в трех различных фазах, предоставляя точки перехвата на разных этапах цикла HTTP-запроса/ответа:
Фаза 1: onRequest
- Назначение: Изменение исходящих запросов до их отправки на сервер.
- Ключевые операции:
– Внесение заголовков аутентификации (например, JWT-токенов в заголовкеAuthorization).
– Проверка параметров запроса перед отправкой.
– Добавление API-ключей или заголовков локализации.
– Отмена недействительных запросов (например, запросов с отсутствующими обязательными полями).
Фаза 2: onResponse
- Назначение: Обработка успешных ответов сервера после их получения, но до обработки приложением.
- Ключевые операции:
– Нормализация форматов данных
– Извлечение метаданных из заголовков ответа (например, токены пагинации, лимиты скорости).
Фаза 3: onError
- Назначение: Обработка неудачных запросов и реализация механизмов восстановления после ошибок.
- Ключевые операции:
– Повторные попытки при временных сбоях (например, тайм-аутах сети, ошибках сервера 5xx).
– Перенаправление пользователей на экран входа после истечения сессии (HTTP 401 Unauthorized).
– Логирование ошибок в инструменты мониторинга для отладки и анализа.
4. Как реализовать перехватчик повторных попыток
Теперь сосредоточимся на основной теме: реализация перехватчика повторных попыток. Перехватчики повторных попыток важны для автоматического управления временными сбоями сети, что повышает устойчивость приложения и улучшает пользовательский опыт.
4.1. Понимание стратегий повторных попыток: интеллектуальные повторные попытки
Простое повторение каждой неудачной запроса без разбора может быть неэффективным и вредным. Интеллектуальные стратегии повторных попыток необходимы для эффективного восстановления после ошибок. Эти стратегии включают:
- Целевые временные ошибки: Повторяйте попытки только в случае ошибок, которые, вероятно, временные:
– Тайм-ауты сети (DioExceptionType.connectionTimeout,DioExceptionType.receiveTimeout,DioExceptionType.sendTimeout,DioExceptionType.connectionError).
– Ошибки сервера HTTP 5xx (например, 500 Internal Server Error, 503 Service Unavailable).
– Ошибки, специфичные для приложения, указывающие на временные проблемы (например, превышение лимита скорости). - Избежание невременных ошибок: Не повторяйте ошибки, указывающие на постоянные проблемы или требующие изменений на стороне клиента:
– Ошибки клиента HTTP 4xx (например, 400 Bad Request, 404 Not Found).
– Ошибки проверки запроса. - Экспоненциальный отказ с джиттером: Реализуйте увеличивающиеся задержки между повторными попытками, чтобы избежать перегрузки сервера, и включайте случайность (джиттер), чтобы предотвратить штормы повторных попыток.
- Селективные повторные попытки: Применяйте логику повторных попыток только к определенным типам запросов или API-эндпоинтам по мере необходимости, обеспечивая тонкое управление.
Реализуя эти интеллектуальные стратегии повторных попыток внутри перехватчика Dio, вы можете создавать более устойчивые и дружелюбные к пользователю приложения Flutter.
4.2. Перехватчик повторных попыток с Dio: реализация кода
Давайте реализуем RetryInterceptor в Flutter с использованием Dio, включая экспоненциальный отказ и селективные повторные попытки. Вот код на Dart для класса RetryInterceptor:
import 'dart:async';
import 'dart:math';
import 'package:dio/dio.dart';
import '../models/retry_config.dart';
class RetryInterceptor extends Interceptor {
static const retryEnabledKey = 'retry_enabled';
static const retryErrorConditionsKey = 'retry_error_conditions';
RetryInterceptor({
required this.dio,
required this.retryConfig,
});
final Dio dio;
final RetryConfig retryConfig;
@override
Future onError(DioException err, ErrorInterceptorHandler handler) async {
final retryCount = err.requestOptions.retryCount + 1;
if (!err.requestOptions.isRetryEnabled(retryEnabledKey) ||
!_shouldRetry(err, retryCount, retryConfig)) {
return handler.next(err);
}
err.requestOptions.retryCount = retryCount;
final delay = _calculateDelay(retryCount);
print(
'Retry attempt: $retryCount, Delay: ${delay.inMilliseconds}ms, Error: ${err.message}');
if (delay != Duration.zero) {
await Future.delayed(delay);
}
try {
final response = await dio.request(
err.requestOptions.path,
options: Options(
method: err.requestOptions.method,
headers: err.requestOptions.headers,
extra: err.requestOptions.extra,
),
cancelToken: err.requestOptions.cancelToken,
data: err.requestOptions.data,
queryParameters: err.requestOptions.queryParameters,
);
return handler.resolve(response);
} on DioException catch (e) {
return super.onError(e, handler);
} catch (_) {
return handler.reject(err);
}
}
Duration _calculateDelay(int retryCount) {
final random = Random();
final jitter = random.nextDouble() * retryConfig.baseDelayInMiliseconds;
final delay = retryConfig.baseDelayInMiliseconds * pow(2, retryCount - 1);
return Duration(milliseconds: (delay + jitter).toInt());
}
bool _shouldRetry(
DioException error, int retryCount, RetryConfig retryConfig) {
if (retryCount > retryConfig.maxRetries) {
return false;
}
final retryErrorStatusCodes =
error.requestOptions.getRetryErrorStatusCodes(retryErrorConditionsKey);
final isRetryableError = error.response != null &&
retryErrorStatusCodes.contains(error.response?.statusCode);
return error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.sendTimeout ||
error.type == DioExceptionType.connectionError ||
isRetryableError;
}
}
extension RequestOptionsRetryExtension on RequestOptions {
bool isRetryEnabled(String key) => (extra[key] as bool?) ?? false;
List<int> getRetryErrorStatusCodes(String key) {
final statusCodes = extra[key] as List<int>?;
return statusCodes ?? [];
}
int get retryCount => (extra['retryCount'] as int?) ?? 0;
set retryCount(int value) => extra['retryCount'] = value;
}
4.3. Разбор кода: ключевые компоненты
Давайте рассмотрим основные части кода RetryInterceptor чтобы быстро понять его функциональность:
RetryInterceptorКласс и константы: Этот класс является основой механизма повторных попыток, расширяяInterceptorDio.retryEnabledKeyиretryErrorConditionsKey— это константы, используемые для селективного управления повторными попытками для отдельных запросов черезRequestOptions.extra.onErrorМетод: обработка ошибок и оркестровка повторных попыток: Это центральный метод, который автоматически вызывается Dio при ошибках запроса. Он управляет всем процессом повторных попыток:
– Проверка повторных попыток: проверяет, включены ли повторные попытки и нужны ли они с помощьюisRetryEnabled()и_shouldRetry().
– Расчет задержки: рассчитывает задержку с помощью_calculateDelay().
– Перевыполнение запроса: повторно отправляет запрос с помощьюdio.request().
– Цепочка разрешения: обрабатывает успех или неудачу._shouldRetryМетод: логика принятия решения о повторной попытке: Этот метод определяет если необходима повторная попытка, проверяя лимиты повторных попыток,DioExceptionTypeи пользовательские коды состояния._calculateDelayМетод: стратегия задержки повторных попыток: Реализует экспоненциальный отказ с джиттером для расчета задержек.RetryConfig:
class RetryConfig {
final int maxRetries;
final int baseDelayInMiliseconds;
const RetryConfig({
this.maxRetries = 3,
this.baseDelayInMiliseconds = 1000,
});
}
RetryConfig: настраивает параметры повторных попыток:maxRetries: максимальное количество попыток (по умолчанию: 3).baseDelayInMiliseconds: базовая задержка для экспоненциального отказа (по умолчанию:1000ms).
Код RetryInterceptor уже поддерживает селективные повторные попытки с использованием retryEnabledKey и retryErrorConditionsKey в requestOptions.extra. Вот как его интегрировать:
4.4. Включение/отключение повторных попыток для каждого запроса
Включите повторные попытки для конкретных запросов, добавив retryEnabledKey: true в карту extra в параметрах запроса Dio:
final response = await dio.get('/api/endpoint',
options: Options(extra: {RetryInterceptor.retryEnabledKey: false}));
Если retryEnabledKey не указан, повторные попытки отключены по умолчанию.
4.5. Указание кодов состояния для повторных попыток на запрос
Повторяйте запросы на основе конкретных кодов состояния HTTP, добавив retryErrorConditionsKey с списком кодов состояния в extra:
final response = await dio.get('/api/endpoint',
options: Options(extra: {
RetryInterceptor.retryErrorConditionsKey: [408, 504], // Retry 408 and 504
}));
Этот пример повторяет запросы, которые завершаются ошибками с кодами состояния 408 (Request Timeout) или 504 (Gateway Timeout), помимо ошибок по умолчанию DioExceptionType.
Поведение повторных попыток по умолчанию определяется RetryConfig предоставленным для RetryInterceptor. Настройте пользовательский класс данных, чтобы при необходимости изменить настройки повторных попыток по умолчанию.
4.6. Looking Ahead: Possible Improvements
Хотя RetryInterceptor предоставляет надежную основу, вот несколько способов, которыми вы можете его дополнительно улучшить:
- Remote Configuration: Для еще большей гибкости рассмотрите возможность использования Firebase Remote Config или аналогичных сервисов для удаленного управления значениями
RetryConfig(например,maxRetriesиbaseDelayInMiliseconds). Это позволит вам изменять стратегию повторных попыток на лету без обновления приложения, что особенно полезно для реагирования на изменяющиеся условия сети или нагрузку сервера. - Idempotency Reminder: Важно помнить, что механизмы повторных попыток наиболее эффективны для idempotent requests — запросов, которые можно безопасно повторять без вызова нежелательных побочных эффектов. Убедитесь, что конечные точки API, к которым применяются повторные попытки, разработаны с учетом идемпотентности. Для неидемпотентных запросов (например, некоторых типов операций записи) вам, возможно, потребуется реализовать дополнительные меры предосторожности или вообще избегать повторных попыток.
5. Wrapping Up
Реализация интерсепторов повторных попыток с Dio — отличный способ повысить надежность ваших приложений Flutter! Давайте повторим ключевые моменты, которые стоит запомнить:
- Interceptors are powerful middleware в Dio, позволяющие централизовать сетевую логику и поддерживать чистоту кода.
- Retry interceptors automatically handle transient errors, делая ваше приложение более устойчивым к нестабильной сети.
- Intelligent retry strategies, такие как экспоненциальный откат и выборочные повторные попытки, критически важны для эффективного восстановления после ошибок.
- Dio makes it easy to implement retry logic с интерсепторами и параметрами запросов, предоставляя вам точный контроль.
Внедряя эти техники, вы можете значительно улучшить надежность вашего приложения и предоставить лучший пользовательский опыт.
Спасибо за чтение 🚀 и счастливого кодинга в вашем пути Flutter!