Intégrations tierces

Avec Dio

[object Object]

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

pubspec.yaml
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>.

lib/data/user_api.dart
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();
  }
}
lib/features/user_screen.dart
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

lib/core/qora_setup.dart
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

lib/features/profile_screen.dart
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:

lib/core/dio_setup.dart
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:

lib/core/dio_setup.dart
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:

lib/data/todo_api.dart
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;
    }
  }
}
lib/features/todo_screen.dart
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:

test/data/todo_api_test.dart
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

Copyright © 2026