Avec BLoC
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é | BLoC | Qora |
|---|---|---|
| Récupérer le profil utilisateur | QoraBuilder | |
| Récupérer une liste de produits / feed infini | QoraBuilder / InfiniteQueryBuilder | |
| Créer, mettre à jour, supprimer (mutations) | QoraMutationBuilder | |
| Flux de paiement multi-étapes | CheckoutBloc | |
| État de validation de formulaire | BLoC ou ValueNotifier | |
| Flux d'authentification | AuthBloc | |
| UI optimiste + invalidation de cache après écriture | setQueryData + 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 :
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
}
});
}
}
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
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 :
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 :
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
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 deMultiBlocProvider - Identifier les BLoCs qui ne font que
emit(Loading) -> fetch -> emit(Loaded/Error)et les remplacer parQoraBuilder - Conserver les BLoCs qui modélisent des workflows multi-événements, des transitions complexes ou de la logique de domaine non-fetch
- Supprimer
BlocProviderpour les blocs de données uniquement - Remplacer
BlocBuilderpour les blocs de données uniquement parQoraBuilder - Injecter
QoraClientdans les BLoCs qui effectuent des écritures et ont besoin d'invalider le cache
Voir aussi
Avec Provider
Ajouter Qora à une app Provider existante pour spécialiser la gestion de l'état serveur tout en conservant Provider pour l'état UI et l'injection de dépendances.
Avec Riverpod
Ajouter Qora à un projet Riverpod pour compléter FutureProvider avec SWR, le comportement hors ligne et les APIs de requête orientées hooks.