Usage avec Flutter

Hooks (flutter_hooks)

Utiliser useQuery, useMutation, useInfiniteQuery, useIsFetching et useIsMutating avec flutter_hooks pour une API concise et composable.

Si votre projet utilise flutter_hooks, qora_hooks fournit une API basée sur les hooks avec des primitives composables pour les écrans complexes.

Installation

# Il n'est pas nécessaire d'ajouter `qora` : il est inclus transitivement.
dependencies:
  flutter_hooks: ^0.21.0
  qora_hooks: ^ 1.3.1

useQuery

Récupère les données et reconstruit le widget à chaque changement d'état. Équivalent à QoraBuilder sans callback builder imbriqué.

class UserScreen extends HookWidget {
  final String userId;
  const UserScreen({super.key, required this.userId});

  @override
  Widget build(BuildContext context) {
    final state = useQuery<User>(
      key: ['users', userId],
      fetcher: () => Api.getUser(userId),
      options: const QoraOptions(staleTime: Duration(minutes: 5)),
    );

    return switch (state) {
      Initial()                    => const SizedBox.shrink(),
      Loading(:final previousData) => previousData != null
          ? UserCard(previousData)
          : const CircularProgressIndicator(),
      Success(:final data)         => UserCard(data),
      Failure(:final error)        => ErrorView(error),
    };
  }
}

Paramètres de useQuery

ParamètreTypeDescription
keyList<Object?>Identifiant de requête. Un changement déclenche un refetch.
fetcherFuture<T> Function()Fonction asynchrone retournant les données.
optionsQoraOptions?Configuration de cache et de retry par requête.

Valeur de retour de useQuery

Retourne un QoraState<T> : classe scellée à quatre variantes : Initial, Loading, Success, Failure.


useMutation

Déclenche une mutation et suit son état.

class EditProfileScreen extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final mutation = useMutation<User, UpdateUserInput>(
      mutator: (input) => Api.updateUser(input),
      options: MutationOptions(
        invalidates: [['users', currentUserId]],
      ),
    );

    return ElevatedButton(
      onPressed: mutation.isPending
          ? null
          : () => mutation.mutate(UpdateUserInput(name: 'Alice')),
      child: mutation.isPending
          ? const CircularProgressIndicator()
          : const Text('Sauvegarder'),
    );
  }
}

Paramètres de useMutation

ParamètreTypeDescription
mutatorFuture<TData> Function(TVariables)Fonction asynchrone exécutant la mutation.
optionsMutationOptions?Callbacks de cycle de vie et configuration de retry.

Valeur de retour de useMutation

Retourne un MutationHandle<TData, TVariables> :

PropriétéTypeDescription
mutatevoid Function(TVariables)Déclenche sans attendre ; les erreurs vont dans l'état.
mutateAsyncFuture<TData?> Function(TVariables)Retourne le résultat ou null en cas d'échec.
resetvoid Function()Réinitialise l'état à MutationIdle.
isIdlebooltrue quand aucune mutation n'a encore été exécutée.
isPendingbooltrue pendant l'exécution de la mutation.
isSuccessbooltrue après une complétion réussie.
isErrorbooltrue après un échec.
dataTData?Disponible quand isSuccess.
errorObject?Disponible quand isError.

useQueryClient

Retourne le QoraClient le plus proche dans l'arbre de widgets.

final client = useQueryClient();
client.invalidate(['posts']);

Le widget doit être encapsulé dans un QoraScope pour que cela fonctionne.


useIsFetching

Retourne true tant qu'au moins une requête gérée par le QoraClient le plus proche a une requête réseau en cours. Utile pour un indicateur de chargement global en haut de l'écran sans le coupler à une requête individuelle.

class AppLoadingBar extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final isFetching = useIsFetching();
    return AnimatedOpacity(
      opacity: isFetching ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 200),
      child: const LinearProgressIndicator(),
    );
  }
}

Valeur de retour de useIsFetching

bool : true quand isFetchingCount > 0, false sinon. Reconstruit le widget uniquement quand la valeur franchit la limite zéro.


useIsMutating

Retourne true tant qu'au moins une mutation gérée par le QoraClient le plus proche est à l'état MutationPending. Pratique pour désactiver un bouton de soumission global pendant qu'une opération d'écriture est en cours.

class SubmitButton extends HookWidget {
  final VoidCallback onPressed;
  const SubmitButton({super.key, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    final isMutating = useIsMutating();
    return FilledButton(
      onPressed: isMutating ? null : onPressed,
      child: isMutating
          ? const SizedBox.square(
              dimension: 18,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          : const Text('Sauvegarder'),
    );
  }
}

Valeur de retour de useIsMutating

bool : true quand activeMutations.length > 0, false sinon.


useInfiniteQuery

Gère les données paginées : accumule les pages et expose une fonction fetchNextPage.

class PostsScreen extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final query = useInfiniteQuery<PostsPage, String?>(
      key: const ['posts'],
      fetcher: (cursor) => Api.getPosts(cursor: cursor),
      getNextPageParam: (page) => page.nextCursor,
      initialPageParam: null,
    );

    final posts = query.pages.expand((p) => p.posts).toList();

    return ListView.builder(
      itemCount: posts.length + (query.hasNextPage ? 1 : 0),
      itemBuilder: (context, i) {
        if (i == posts.length) {
          if (!query.isFetchingNextPage) query.fetchNextPage();
          return const Center(child: CircularProgressIndicator());
        }
        return PostTile(posts[i]);
      },
    );
  }
}

Paramètres de useInfiniteQuery

ParamètreTypeDescription
keyList<Object?>Identifiant de requête. Un changement déclenche un refetch.
fetcherFuture<TData> Function(TPageParam)Appelé avec le paramètre de page pour chaque page.
getNextPageParamTPageParam? Function(TData)Retourne null pour signaler la dernière page.
initialPageParamTPageParamPremier paramètre de page passé à fetcher.

Valeur de retour de useInfiniteQuery

Retourne un InfiniteQueryHandle<TData, TPageParam> :

PropriétéTypeDescription
pagesList<TData>Toutes les pages récupérées dans l'ordre.
isLoadingbooltrue pendant le chargement de la première page.
isFetchingNextPagebooltrue pendant le chargement d'une page suivante.
hasNextPageboolfalse quand plus aucune page n'est disponible.
fetchNextPageFuture<void> Function()Charge la page suivante. Sans effet si occupé.
errorObject?Dernière erreur, ou null.

Composition de hooks

Les hooks se composent naturellement. L'exemple suivant récupère un utilisateur, met à jour son profil et affiche une erreur inline dans un seul widget, sans setState ni StreamBuilder.

class ProfileScreen extends HookWidget {
  final String userId;
  const ProfileScreen({super.key, required this.userId});

  @override
  Widget build(BuildContext context) {
    final client = useQueryClient();
    final userQuery = useQuery<User>(
      key: ['users', userId],
      fetcher: () => Api.getUser(userId),
    );
    final updateMutation = useMutation<User, UpdateUserInput>(
      mutator: Api.updateUser,
      options: MutationOptions(
        invalidates: [['users', userId]],
      ),
    );

    return switch (userQuery) {
      Loading()             => const CircularProgressIndicator(),
      Failure(:final error) => ErrorView(error),
      Success(:final data)  => ProfileForm(
        user: data,
        isSaving: updateMutation.isPending,
        saveError: updateMutation.error,
        onSave: (input) => updateMutation.mutate(input),
      ),
      _ => const SizedBox.shrink(),
    };
  }
}

qora_hooks requiert que votre widget étende HookWidget (ou HookConsumerWidget si vous combinez avec Riverpod). StatelessWidget et StatefulWidget standard ne sont pas pris en charge : utiliser QoraBuilder et QoraMutationBuilder à la place.
Copyright © 2026