Avec Dio
Dio est le client HTTP le plus populaire dans l'écosystème Dart/Flutter. Ce guide couvre l'utilisation idiomatique de Qora+Dio: mapping d'erreurs, intercepteurs pour les tokens d'authentification, et des fonctions fetcher prêtes à l'emploi.
Configuration
dependencies:
qora: ^1.3.1
dio: ^5.0.0
Pattern de Base: Dio comme Fetcher
La fonction fetcher passée à QoraClient.fetchQuery ou QoraBuilder doit utiliser Dio pour l'appel HTTP et laisser les exceptions Dio se propager. Qora les intercepte, applique la logique de retry et expose l'erreur via QoraState<Failure>.
import 'package:dio/dio.dart';
import 'package:qora/qora.dart';
final api = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
class UserApi {
Future<User> getUser(String id) async {
final res = await api.get('/users/$id');
return User.fromJson(res.data);
}
Future<List<User>> listUsers() async {
final res = await api.get('/users');
return (res.data as List).map((j) => User.fromJson(j)).toList();
}
}
QoraBuilder<User>(
queryKey: ['user', userId],
fetcher: () => userApi.getUser(userId),
builder: (context, state, _) => switch (state) {
Success(:final data) => UserCard(data),
Loading() => const CircularProgressIndicator(),
Failure(:final error) => ErrorView(error),
_ => const SizedBox.shrink(),
},
)
Mapping d'Erreurs: DioException vers Erreurs Applicatives
Par défaut, Qora stocke le DioException brut dans l'état Failure. Enregistrez un errorMapper global sur QoraClientConfig pour convertir les erreurs Dio en erreurs applicatives typées, ou utilisez QoraOptions.transformError par requête.
Error Mapper Global
import 'package:dio/dio.dart';
import 'package:qora/qora.dart';
/// Erreur applicative typée. `errorMapper` doit retourner une `QoraException`, d'où l'héritage.
class AppError extends QoraException {
final int? statusCode;
const AppError(super.message, {this.statusCode, super.originalError, super.stackTrace});
}
final client = QoraClient(
config: QoraClientConfig(
errorMapper: (Object error, StackTrace? stackTrace) {
if (error is DioException) {
return switch (error.type) {
DioExceptionType.connectionTimeout => AppError('Connection timed out', originalError: error),
DioExceptionType.receiveTimeout => AppError('Server is not responding', originalError: error),
DioExceptionType.badResponse => _mapHttpStatus(error),
DioExceptionType.connectionError => AppError('No internet connection', originalError: error),
_ => AppError(error.message ?? 'Request failed', originalError: error),
};
}
if (error is QoraException) return error;
return QoraException(error.toString(), originalError: error, stackTrace: stackTrace);
},
),
);
AppError _mapHttpStatus(DioException error) {
final status = error.response?.statusCode ?? 0;
final message = error.response?.data?['message'] as String? ?? 'Request failed';
return AppError(message, statusCode: status, originalError: error);
}
Transformation d'Erreur par Requête
QoraBuilder<Profile>(
queryKey: ['profile'],
fetcher: () => api.get('/profile').then((r) => Profile.fromJson(r.data)),
options: QoraOptions(
transformError: (error) {
if (error is DioException && error.type == DioExceptionType.badResponse) {
return ApiError.fromResponse(error.response!);
}
return error;
},
),
builder: (context, state, _) { ... },
)
Intercepteur d'Authentification
Utilisez un intercepteur Dio pour attacher un token d'accès à chaque requête. La source du token (ex: stockage sécurisé) est consultée à chaque requête:
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = const FlutterSecureStorage();
final api = Dio(BaseOptions(baseUrl: 'https://api.example.com'))
..interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await storage.read(key: 'access_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
));
Intercepteur de Rafraîchissement de Token
Quand le serveur retourne un 401, tentez un rafraîchissement silencieux du token, puis réessayez la requête originale exactement une fois:
final api = Dio(BaseOptions(baseUrl: 'https://api.example.com'))
..interceptors.addAll([
_AuthInterceptor(),
_TokenRefreshInterceptor(api, storage),
]);
class _AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await storage.read(key: 'access_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
}
class _TokenRefreshInterceptor extends QueuedInterceptor {
_TokenRefreshInterceptor(this._dio, this._storage);
final Dio _dio;
final FlutterSecureStorage _storage;
bool _isRefreshing = false;
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode != 401 || _isRefreshing) {
return handler.next(err);
}
_isRefreshing = true;
try {
final refreshToken = await _storage.read(key: 'refresh_token');
if (refreshToken == null) {
_isRefreshing = false;
return handler.next(err);
}
final res = await _dio.post('/auth/refresh',
data: {'refresh_token': refreshToken});
final newAccessToken = res.data['access_token'] as String;
await _storage.write(key: 'access_token', value: newAccessToken);
_isRefreshing = false;
// Réessayer la requête originale avec le nouveau token.
final opts = err.requestOptions;
opts.headers['Authorization'] = 'Bearer $newAccessToken';
final retryResponse = await _dio.fetch(opts);
return handler.resolve(retryResponse);
} catch (e) {
_isRefreshing = false;
return handler.next(err);
}
}
}
Le mécanisme de retry de Qora fonctionne au-dessus de cette couche. Si le rafraîchissement du token échoue ou que la requête réessayée échoue encore, le DioException se propage à Qora, qui applique sa propre logique retryCount / retryDelay.
Fetcher Complet avec Typage d'Erreur
Pour un fetcher entièrement typé retournant des modèles domaine:
class TodoApi {
TodoApi(this._api);
final Dio _api;
Future<Todo> getTodo(String id) async {
try {
final res = await _api.get('/todos/$id');
return Todo.fromJson(res.data);
} on DioException {
rethrow; // laisser errorMapper de Qora le gérer
}
}
Future<List<Todo>> listTodos({String? filter}) async {
try {
final res = await _api.get('/todos', queryParameters: {
if (filter != null) 'filter': filter,
});
return (res.data as List).map((j) => Todo.fromJson(j)).toList();
} on DioException {
rethrow;
}
}
}
QoraBuilder<List<Todo>>(
queryKey: ['todos', filter],
fetcher: () => todoApi.listTodos(filter: filter),
options: const QoraOptions(
staleTime: Duration(minutes: 1),
retryCount: 2,
),
builder: (context, state, _) {
if (state.errorOrNull case final error?) {
return TodoErrorView(error, onRetry: () => context.qora.invalidate(['todos', filter]));
}
return ListView.builder(
itemCount: (state.dataOrNull ?? []).length,
itemBuilder: (_, i) => TodoTile(state.dataOrNull![i]),
);
},
)
Tester les Fetchers Dio
Utilisez Dio avec un adaptateur remplacé (HttpClientAdapter ou DioMixin) pour que les tests s'exécutent sans vrai réseau:
import 'package:dio/dio.dart';
import 'package:qora/qora.dart';
import 'package:test/test.dart';
void main() {
late Dio dio;
late QoraClient client;
setUp(() {
dio = Dio(BaseOptions(baseUrl: 'https://test.example.com'));
// Remplacer par un adaptateur mock pour les tests hors-ligne.
dio.httpClientAdapter = _MockAdapter();
client = QoraClient();
});
test('le succès du fetcher arrive dans l\'état Success de Qora', () async {
final result = await client.fetchQuery<Todo>(
key: ['todo', 1],
fetcher: () async {
final res = await dio.get('/todos/1');
return Todo.fromJson(res.data);
},
);
expect(result.title, 'Test Todo');
expect(client.getQueryState<Todo>(['todo', 1]), isA<Success<Todo>>());
});
}
Étapes Suivantes
- Gestion d'Erreurs:
errorMapperglobal ettransformErrorpar requête - API QoraOptions:
retryCount,retryDelay,retryCondition - Mutations: Opérations d'écriture avec Dio et mises à jour optimistes
- Réseau Adaptatif: Dio avec patterns offline-first et
NetworkMode
Avec RxDart
Gestion réactive des clés avec RxDart et QoraClient, commutation dynamique de requêtes avec nettoyage garanti des ressources.
Avec GraphQL
Envelopper les clients GraphQL graphql ou ferry dans des fetchers Qora pour une gestion fluide des requêtes avec stale-while-revalidate, invalidation de cache et pagination.