Usage avec Flutter

QoraBuilder

Le widget qui récupère les données, s'abonne aux changements d'état et reconstruit l'UI à chaque transition d'état.

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

user_list_screen.dart
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 :

  1. context : le BuildContext
  2. state : le QoraState<T> courant (ce que vous avez)
  3. fetchStatus : le FetchStatus courant (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 :

  1. 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).
  2. Récupère via client.fetchQuery(...) si enabled est true. 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

PropTypeDéfautDescription
queryKeyObject (List ou QoraKey)requisClé de cache. La changer déclenche un réabonnement et un fetch frais.
fetcherFuture<T> Function()requisFonction asynchrone qui retourne les données. Doit lever une exception en cas d'échec.
builderWidget Function(BuildContext, QoraState<T>, FetchStatus)requisConstruit l'arbre de widgets depuis l'état courant et le statut de fetch.
optionsQoraOptions?nullConfig par requête fusionnée sur QoraClientConfig.defaultOptions.
enabledbooltrueQuand false, aucun fetch n'est déclenché. Le widget observe quand même l'état en cache.
keepPreviousDataboolfalseEnrichit les états Loading/Failure avec les dernières données Success de ce widget.
selectObject? Function(T data)?nullFiltre 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.
clientQoraClient?nullSurcharge 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 :

user_screen.dart
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 :

user_list_screen.dart
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 FetchStatusSignification
FetchStatus.fetchingUne requête réseau est en cours.
FetchStatus.pausedHors-ligne : le fetch est en file et sera rejoué à la reconnexion.
FetchStatus.idleAucun 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 :

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

user_list_screen.dart
// 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é :

paginated_posts_screen.dart
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 :

user_screen.dart
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 :

notifications_screen.dart
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 :

user_screen.dart
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ètreDéfautDescription
initialDatanullDonnées placeholder de type T pour pré-remplir le cache.
initialDataUpdatedAtepochHorodatage pour le calcul de péremption. DateTime.now() pour ignorer le réseau.
placeholderDatanullFonction paresseuse qui dérive les données initiales d'une autre entrée du cache.

Quand initialData est fourni :

  • Si frais (updatedAt dans staleTime) : 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 :

  1. Annule l'abonnement au stream précédent.
  2. S'abonne au stream de la nouvelle clé (en partant de son état courant en cache : peut être Initial, Success, etc.).
  3. Déclenche un fetchQuery frais pour la nouvelle clé (si enabled).

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é.

notification_badge.dart
// 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

PropTypeDescription
queryKeyObject (List ou QoraKey)Clé à observer.
builderWidget Function(BuildContext, QoraState<T>)Construit l'arbre de widgets depuis l'état courant.
clientQoraClient?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

Copyright © 2026