QoraBuilder
QoraBuilder<T> est le moyen principal de lier les données serveur à l'UI Flutter. Il gère le cycle de vie complet des requêtes : récupération, cache, réactivité, revalidation en arrière-plan, avec une seule déclaration de widget.
Utilisation de base
QoraBuilder<List<User>>(
queryKey: ['users'],
fetcher: () => api.getUsers(),
builder: (context, state, fetchStatus) {
return switch (state) {
Initial() => const SizedBox.shrink(),
Loading(:final previousData) => previousData != null
? Stack(children: [UserList(previousData), const LinearProgressIndicator()])
: const CircularProgressIndicator(),
Success(:final data) => UserList(data),
Failure(:final error, :final previousData) => previousData != null
? Column(children: [UserList(previousData), ErrorBanner(error)])
: ErrorScreen(error),
};
},
)
Le builder reçoit trois arguments :
context: leBuildContextstate: leQoraState<T>courant (ce que vous avez)fetchStatus: leFetchStatuscourant (ce que Qora fait en ce moment)
La correspondance de motifs exhaustive sur le state scellé est l'approche idiomatique. Utiliser fetchStatus pour les indicateurs secondaires (barres de progression, chips hors-ligne) sans modifier la vue d'état principale.
Fonctionnement
Au montage, QoraBuilder :
- S'abonne à
client.watchState<T>(queryKey): un stream d'observation uniquement qui émet chaque mise à jour d'état pour cette clé (y compris les mises à jour déclenchées par d'autres widgets ou services en arrière-plan). - Récupère via
client.fetchQuery(...)sienabledesttrue. Le client gère automatiquement la déduplication, le cache et le stale-while-revalidate.
Quand la requête est invalidée externalement (via client.invalidate(), une reprise du cycle de vie ou un événement de reconnexion), l'état passe à Loading(previousData: data). QoraBuilder détecte ce pattern dans le stream et déclenche un fetchQuery frais. La déduplication du client garantit qu'une seule requête réseau se déclenche même si plusieurs widgets sont abonnés à la même clé.
Props
| Prop | Type | Défaut | Description |
|---|---|---|---|
queryKey | Object (List ou QoraKey) | requis | Clé de cache. La changer déclenche un réabonnement et un fetch frais. |
fetcher | Future<T> Function() | requis | Fonction asynchrone qui retourne les données. Doit lever une exception en cas d'échec. |
builder | Widget Function(BuildContext, QoraState<T>, FetchStatus) | requis | Construit l'arbre de widgets depuis l'état courant et le statut de fetch. |
options | QoraOptions? | null | Config par requête fusionnée sur QoraClientConfig.defaultOptions. |
enabled | bool | true | Quand false, aucun fetch n'est déclenché. Le widget observe quand même l'état en cache. |
keepPreviousData | bool | false | Enrichit les états Loading/Failure avec les dernières données Success de ce widget. |
select | Object? Function(T data)? | null | Filtre de reconstruction : le widget ne se reconstruit que si le type d'état ou cette valeur change. Le builder reçoit toujours l'état complet. |
client | QoraClient? | null | Surcharge le client du QoraScope. Utile pour les tests ou les scopes imbriqués. |
Patterns
Traiter les quatre états
Utiliser .when() pour la correspondance exhaustive ou .maybeWhen() quand seuls certains états sont pertinents :
QoraBuilder<User>(
queryKey: ['users', userId],
fetcher: () => api.getUser(userId),
builder: (context, state, fetchStatus) {
return state.when(
onInitial: () => const SizedBox.shrink(),
onLoading: (previousData) => previousData != null
? UserCard(user: previousData, isRefreshing: true)
: const CircularProgressIndicator(),
onSuccess: (user, updatedAt) => UserCard(user: user),
onError: (error, stackTrace, previousData) => ErrorScreen(
message: error.toString(),
onRetry: () => context.qora.invalidate(['users', userId]),
),
);
},
)
UI adaptée au réseau avec FetchStatus
FetchStatus est le second axe aux côtés de QoraState. L'utiliser pour afficher un indicateur de rafraîchissement en arrière-plan ou un message "hors-ligne : en attente" sans modifier la vue d'état principale :
QoraBuilder<List<User>>(
queryKey: ['users'],
fetcher: api.getUsers,
builder: (context, state, fetchStatus) {
return switch (state) {
Initial() || Loading(previousData: null)
when fetchStatus == FetchStatus.paused =>
const Center(child: Text('Hors-ligne : connectez-vous pour charger')),
Initial() || Loading() =>
const CircularProgressIndicator(),
Success(:final data) => Column(
children: [
if (fetchStatus == FetchStatus.fetching)
const LinearProgressIndicator(),
if (fetchStatus == FetchStatus.paused)
const OfflineSyncChip(),
UserList(data),
],
),
Failure(:final error) => ErrorScreen(error),
};
},
)
Valeur FetchStatus | Signification |
|---|---|
FetchStatus.fetching | Une requête réseau est en cours. |
FetchStatus.paused | Hors-ligne : le fetch est en file et sera rejoué à la reconnexion. |
FetchStatus.idle | Aucun fetch actif (les données sont fraîches ou l'état est terminal). |
FetchStatus.paused n'apparaît que quand QoraOptions.networkMode est NetworkMode.online (le défaut) et que l'appareil n'a pas de connectivité. Avec NetworkMode.always, les fetchs s'exécutent quelle que soit la connectivité et cet état n'est jamais émis.Requêtes conditionnelles (enabled)
Empêcher le fetch jusqu'à ce qu'une dépendance soit disponible :
QoraBuilder<Profile>(
queryKey: ['profiles', userId],
fetcher: () => api.getProfile(userId!),
enabled: userId != null, // Pas de fetch tant que userId n'est pas résolu
builder: (context, state, fetchStatus) { ... },
)
Quand enabled passe de false à true (via setState), un fetch est déclenché automatiquement.
Performance: abonnements dérivés avec select
select permet de dériver une valeur calculée des données. Le widget ne se reconstruit que quand la valeur sélectionnée change (comparaison ==). Utilisez-le pour éviter les reconstructions inutiles:
// Reconstruire seulement quand le nombre d'utilisateurs change.
QoraBuilder<List<User>>(
queryKey: ['users'],
fetcher: () => api.getUsers(),
select: (users) => users.length,
builder: (context, state, fetchStatus) => Text('${state.dataOrNull} utilisateurs'),
)
Le builder reçoit toujours le QoraState<T> complet et à jour : select décide quand reconstruire, pas ce qui est passé. Un changement de type d'état (Loading, Success, Failure) ou de FetchStatus reconstruit toujours, donc les erreurs et les spinners ne sont jamais masqués. Sans select, le widget se reconstruit à chaque changement d'état.
Pagination sans scintillement (keepPreviousData)
Quand la clé de requête change (ex. numéro de page incrémenté), la nouvelle clé démarre à l'état Initial, provoquant un spinner plein écran perturbant. keepPreviousData évite cela en portant les dernières données réussies dans la phase de chargement de la nouvelle clé :
QoraBuilder<List<Post>>(
queryKey: ['posts', currentPage],
fetcher: () => api.getPosts(page: currentPage),
keepPreviousData: true,
builder: (context, state, fetchStatus) {
// dataOrNull retourne les données de Success, Loading.previousData, ou Failure.previousData
final posts = state.dataOrNull ?? [];
return Stack(
children: [
PostList(posts: posts),
if (state.isLoading) const LinearProgressIndicator(),
],
);
},
)
keepPreviousData comble uniquement l'écart : si le client fournit déjà previousData dans l'état, cette valeur est prioritaire. Le cache local du widget est purement un fallback.Afficher un horodatage de fraîcheur
Success porte updatedAt : le moment où les données ont été récupérées pour la dernière fois :
onSuccess: (user, updatedAt) => Column(
children: [
UserCard(user: user),
Text(
'Mis à jour ${_timeAgo(updatedAt)}',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
Polling
Passer refetchInterval via options pour effectuer un polling à intervalle fixe :
QoraBuilder<List<Notification>>(
queryKey: ['notifications'],
fetcher: api.getNotifications,
options: const QoraOptions(refetchInterval: Duration(seconds: 30)),
builder: (context, state, fetchStatus) { ... },
)
Le polling est actif uniquement pendant que QoraBuilder est dans l'arbre de widgets. Il s'arrête automatiquement au dispose.
Amorcer les données initiales (initialData)
QoraOptions.initialData amorce le cache avec des données placeholder avant qu'aucun fetch ne soit terminé. Utiliser pour l'hydratation SSR, le rendu instantané depuis SharedPreferences ou une session précédente :
QoraBuilder<User>(
queryKey: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(
initialData: cachedUser, // amorcer le cache
initialDataUpdatedAt: DateTime.now(), // marquer comme frais
staleTime: const Duration(minutes: 5), // ignorer réseau si frais
),
builder: (context, state, fetchStatus) { ... },
)
| Paramètre | Défaut | Description |
|---|---|---|
initialData | null | Données placeholder de type T pour pré-remplir le cache. |
initialDataUpdatedAt | epoch | Horodatage pour le calcul de péremption. DateTime.now() pour ignorer le réseau. |
placeholderData | null | Fonction paresseuse qui dérive les données initiales d'une autre entrée du cache. |
Quand initialData est fourni :
- Si frais (
updatedAtdansstaleTime) : le widget s'affiche immédiatement avec le placeholder et ignore le réseau. - Si périmé (epoch par défaut) : le widget s'affiche avec le placeholder et déclenche un SWR en arrière-plan. Le placeholder est transporté via
Loading.previousData.
initialData a priorité sur placeholderData. Une incompatibilité de type à l'exécution est silencieusement ignorée (le cache reste Initial et fetch normalement).Comportement lors du changement de clé
Quand queryKey change (didUpdateWidget), QoraBuilder :
- Annule l'abonnement au stream précédent.
- S'abonne au stream de la nouvelle clé (en partant de son état courant en cache : peut être
Initial,Success, etc.). - Déclenche un
fetchQueryfrais pour la nouvelle clé (sienabled).
C'est ainsi que la pagination, le changement d'onglet et les changements de filtre sont gérés automatiquement sans gestion d'état supplémentaire.
QoraStateBuilder : observer sans fetcher
QoraStateBuilder<T> s'abonne à l'état d'une requête sans déclencher de fetch. L'utiliser quand un autre widget ou service possède le cycle de vie du fetch et que seul l'état courant doit être affiché.
// NotificationsScreen (un QoraBuilder) possède le fetch.
// Ce badge reflète simplement l'état en cache de la même clé.
QoraStateBuilder<List<Notification>>(
queryKey: ['notifications'],
builder: (context, state) {
final count = state.dataOrNull?.length ?? 0;
return Badge(
label: Text('$count'),
isLabelVisible: count > 0,
child: const Icon(Icons.notifications),
);
},
)
Props de QoraStateBuilder
| Prop | Type | Description |
|---|---|---|
queryKey | Object (List ou QoraKey) | Clé à observer. |
builder | Widget Function(BuildContext, QoraState<T>) | Construit l'arbre de widgets depuis l'état courant. |
client | QoraClient? | Surcharge le client du QoraScope. |
QoraStateBuilder est adapté aux badges, compteurs ou affichages secondaires qui dérivent leurs données d'une requête chargée ailleurs : par exemple, un compteur d'articles dans le panier ou un badge de messages non lus dans une barre d'application.Étapes suivantes
- Guide réseau adaptatif : Configuration hors-ligne complète avec
FetchStatuset file de mutations - Statut réseau : Widgets
NetworkStatusBuilderetNetworkStatusIndicator - États des requêtes : Référence complète de la machine d'état
- QoraOptions : Configuration de stale time, retry, polling et
networkMode - Mises à jour optimistes : UI instantanée avec rollback sécurisé