Intégrations tierces

Avec BLoC

Utiliser Qora pour l'état serveur et BLoC pour les machines à états UI, avec une séparation explicite des préoccupations.

BLoC est efficace pour les machines à états UI complexes telles que les processus de paiement multi-étapes, les flux d'authentification et l'orchestration de formulaires. Il est moins efficace pour les opérations d'état serveur répétitives telles que fetch, cache, retry et revalidation en arrière-plan.

Dans une architecture combinée, BLoC reste responsable des transitions UI et Qora gère les cycles de vie de l'état serveur.


Division du travail

ResponsabilitéBLoCQora
Récupérer le profil utilisateurQoraBuilder
Récupérer une liste de produits / feed infiniQoraBuilder / InfiniteQueryBuilder
Créer, mettre à jour, supprimer (mutations)QoraMutationBuilder
Flux de paiement multi-étapesCheckoutBloc
État de validation de formulaireBLoC ou ValueNotifier
Flux d'authentificationAuthBloc
UI optimiste + invalidation de cache après écrituresetQueryData + invalidate

Les deux coexistent sans friction : QoraScope s'imbrique à l'intérieur de l'arbre BlocProvider (ou vice versa).


L'anti-pattern : BLoC de récupération de données

Un BLoC qui ne fait qu'encapsuler un appel API n'apporte aucun bénéfice structurel par rapport à un appel de fonction, mais porte tout l'overhead des events, states et du câblage :

lib/blocs/user_bloc.dart
abstract class UserEvent {}
class LoadUser extends UserEvent { final int id; LoadUser(this.id); }

abstract class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState { final User user; UserLoaded(this.user); }
class UserError extends UserState { final String message; UserError(this.message); }

class UserBloc extends Bloc<UserEvent, UserState> {
  final UserApi _api;

  UserBloc(this._api) : super(UserInitial()) {
    on<LoadUser>((event, emit) async {
      emit(UserLoading());
      try {
        emit(UserLoaded(await _api.getUser(event.id)));
      } catch (e) {
        emit(UserError('$e')); // pas de retry, pas de déduplication, pas de SWR
      }
    });
  }
}
lib/user_screen.dart
BlocBuilder<UserBloc, UserState>(
  builder: (context, state) => switch (state) {
    UserLoading() => const CircularProgressIndicator(),
    UserLoaded(:final user) => UserCard(user),
    UserError(:final message) => ErrorWidget(message),
    _ => const SizedBox.shrink(),
  },
)

Après : QoraBuilder pour les données serveur

lib/user_screen.dart
QoraBuilder<User>(
  queryKey: ['users', userId],
  fetcher: () => UserApi.getUser(userId),
  options: const QoraOptions(staleTime: Duration(minutes: 5)),
  builder: (context, state, fetchStatus) => switch (state) {
    Success(:final data) => UserCard(data),
    Loading() => const CircularProgressIndicator(),
    Failure(:final error) => ErrorWidget('$error'),
    Initial() => const SizedBox.shrink(),
  },
)

Pour les lectures de données uniquement, Qora supprime le boilerplate event/state et le code de transition manuel. Retry avec backoff, déduplication des requêtes et revalidation en arrière-plan au focus ou à la reconnexion sont intégrés.

Le troisième argument du builder fetchStatus distingue les requêtes actives (FetchStatus.fetching) des requêtes hors ligne mises en pause (FetchStatus.paused).


BLoC + Qora sur le même écran

Les écrans complexes nécessitent souvent les deux : une machine à états UI (BLoC) pilotant une vue qui affiche aussi des données serveur (Qora). Ils se composent sans friction :

lib/checkout_screen.dart
BlocBuilder<CheckoutBloc, CheckoutState>(
  builder: (context, checkoutState) {
    // CheckoutBloc pilote la machine d'étapes
    if (checkoutState is SelectingAddress) {
      return QoraBuilder<List<Address>>(
        queryKey: ['users', userId, 'addresses'],
        fetcher: () => api.getAddresses(userId),
        builder: (context, state, fetchStatus) => switch (state) {
          Success(:final data) => AddressSelector(
              addresses: data,
              onSelect: (address) =>
                context.read<CheckoutBloc>().add(AddressSelected(address)),
            ),
          Loading() => const CircularProgressIndicator(),
          Failure(:final error)=> ErrorWidget('$error'),
          Initial() => const SizedBox.shrink(),
        },
      );
    }
    if (checkoutState is ReviewingOrder) {
      return OrderReviewWidget(order: checkoutState.order);
    }
    return const SizedBox.shrink();
  },
)

BLoC contrôle la sélection des étapes. Qora fournit les données serveur à l'intérieur de chaque étape.


Invalider Qora depuis un BLoC

Quand un BLoC effectue une écriture (soumission de formulaire, étape de paiement complétée), il peut invalider le cache de Qora pour que la prochaine lecture obtienne des données fraîches :

lib/blocs/update_user_bloc.dart
class UpdateUserBloc extends Bloc<UpdateUserEvent, UpdateUserState> {
  final QoraClient _client;
  final UserApi _api;

  UpdateUserBloc(this._client, this._api) : super(UpdateUserInitial()) {
    on<SubmitUpdate>((event, emit) async {
      emit(UpdateUserLoading());
      try {
        await _api.updateUser(event.id, event.name);
        _client.invalidate(['users', event.id]); // Qora refetch à la prochaine lecture
        emit(UpdateUserSuccess());
      } catch (e) {
        emit(UpdateUserError('$e'));
      }
    });
  }
}

Passer QoraClient aux BLoCs qui ont besoin de coordonner avec le cache d'état serveur. Ne pas passer BuildContext, le client est un objet de cache partagé, pas une préoccupation de widget.


Configuration

lib/main.dart
final qoraClient = QoraClient(
  config: const QoraClientConfig(debugMode: kDebugMode),
);

runApp(
  MultiBlocProvider(
    providers: [
      BlocProvider(create: (_) => AuthBloc()),
      BlocProvider(create: (ctx) => CheckoutBloc(ctx.read())),
    ],
    child: QoraScope(
      client: qoraClient,
      lifecycleManager: FlutterLifecycleManager(qoraClient: qoraClient),
      connectivityManager: FlutterConnectivityManager(),
      child: const MyApp(),
    ),
  ),
);

QoraScope peut être imbriqué à l'intérieur ou à l'extérieur de MultiBlocProvider. Choisir la disposition qui donne la composition la plus claire à la racine de l'app.


Checklist

  • Ajouter qora_flutter à pubspec.yaml
  • Ajouter QoraScope à l'arbre de widgets aux côtés de MultiBlocProvider
  • Identifier les BLoCs qui ne font que emit(Loading) -> fetch -> emit(Loaded/Error) et les remplacer par QoraBuilder
  • Conserver les BLoCs qui modélisent des workflows multi-événements, des transitions complexes ou de la logique de domaine non-fetch
  • Supprimer BlocProvider pour les blocs de données uniquement
  • Remplacer BlocBuilder pour les blocs de données uniquement par QoraBuilder
  • Injecter QoraClient dans les BLoCs qui effectuent des écritures et ont besoin d'invalider le cache

Voir aussi

Copyright © 2026