Avec Riverpod
Provider et NotifierProvider de Riverpod sont efficaces pour l'injection de dépendances et l'état client. FutureProvider gère les lectures asynchrones mais ne fournit pas par défaut la sémantique SWR, retry/backoff, ni la pause et le rejeu hors ligne.
Qora ajoute ces capacités d'état serveur sans remplacer Riverpod. Riverpod reste concentré sur les graphes de dépendances et l'état client.
Ce que FutureProvider manque
final userProvider = FutureProvider.family<User, int>((ref, id) async {
return UserApi.getUser(id);
});
final userAsync = ref.watch(userProvider(userId));
return userAsync.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorWidget('$e'),
data: (user) => UserCard(user),
);
Limitations :
- Spinner plein écran à chaque rafraîchissement: les anciennes données sont supprimées, pas de stale-while-revalidate.
- Pas de retry avec backoff: un seul échec est affiché immédiatement.
- Pas de refetch en arrière-plan au focus de l'app ou à la reconnexion.
- Pas de support hors ligne: pas de
FetchStatus, pas de comportement pause/rejeu à la reconnexion. AsyncValuen'a pas depreviousData: pas de chemin de dégradation gracieuse.
Chemin avec les hooks
Si flutter_hooks est déjà utilisé (ce que font souvent les développeurs Riverpod via hooks_riverpod), qora_hooks fournit un hook useQuery qui semble natif :
dependencies:
qora_flutter: ^1.3.1
qora_hooks: ^1.3.1 # nécessite flutter_hooks
class UserScreen extends HookWidget {
const UserScreen({required this.userId, super.key});
final int userId;
@override
Widget build(BuildContext context) {
final state = useQuery<User>(
key: ['users', userId],
fetcher: () => UserApi.getUser(userId),
options: const QoraOptions(staleTime: Duration(minutes: 5)),
);
return switch (state) {
Success(:final data) => UserCard(data),
Loading() => const CircularProgressIndicator(),
Failure(:final error) => ErrorWidget('$error'),
Initial() => const SizedBox.shrink(),
};
}
}
useQuery retourne QoraState<T>, le même type sealed utilisé dans toutes les APIs Qora. Stale-while-revalidate, retry, refetch en arrière-plan et déduplication sont intégrés.
Dégradation gracieuse avec previousData
Loading et Failure portent previousData, qui est la dernière valeur réussie :
final state = useQuery<User>(
key: ['users', userId],
fetcher: () => UserApi.getUser(userId),
);
return switch (state) {
Success(:final data) => UserCard(data),
Loading(:final previousData) when previousData != null =>
Stack(children: [
UserCard(previousData),
const LoadingOverlay(),
]),
Loading() => const CircularProgressIndicator(),
Failure(:final error, :final previousData) when previousData != null =>
Column(children: [
UserCard(previousData),
ErrorBanner('$error'),
]),
Failure(:final error) => ErrorWidget('$error'),
Initial() => const SizedBox.shrink(),
};
Mutations avec useMutation
class UpdateNameForm extends HookWidget {
const UpdateNameForm({required this.userId, super.key});
final int userId;
@override
Widget build(BuildContext context) {
final handle = useMutation<User, String>(
mutator: (name) => UserApi.updateUser(userId, name),
options: MutationOptions(
onSuccess: (data, _, __) async {
QoraScope.of(context).setQueryData(['users', userId], data);
QoraScope.of(context).invalidateWhere((k) => k.firstOrNull == 'users');
},
),
);
return ElevatedButton(
onPressed: handle.isPending ? null : () => handle.mutate(nameController.text),
child: handle.isPending
? const CircularProgressIndicator()
: const Text('Enregistrer'),
);
}
}
MutationHandle expose : isIdle, isPending, isSuccess, isError, data, error, mutate(variables), mutateAsync(variables), reset().
Alternative avec l'API widget
Pour rester avec StatefulWidget plutôt que HookWidget, QoraBuilder fournit les mêmes capacités :
QoraBuilder<User>(
queryKey: ['users', userId],
fetcher: () => UserApi.getUser(userId),
options: const QoraOptions(staleTime: Duration(minutes: 5)),
builder: (context, state, fetchStatus) {
if (fetchStatus == FetchStatus.paused) {
return OfflineBanner(staleData: state.dataOrNull);
}
return switch (state) {
Success(:final data) => UserCard(data),
Loading() => const CircularProgressIndicator(),
Failure(:final error) => ErrorWidget('$error'),
Initial() => const SizedBox.shrink(),
};
},
)
Le troisième argument du builder fetchStatus expose FetchStatus.paused en états hors ligne. Utiliser QoraBuilder quand ce signal est requis par l'UI.
Conserver Riverpod pour l'injection de dépendances et l'état client
Provider et NotifierProvider de Riverpod sont inégalés pour l'injection de dépendances et l'état client. Les deux périmètres se composent proprement :
final qoraClient = QoraClient(
config: const QoraClientConfig(debugMode: kDebugMode),
);
runApp(
ProviderScope(
overrides: [
userApiProvider.overrideWithValue(UserApi(baseUrl: env.apiUrl)),
],
child: QoraScope(
client: qoraClient,
lifecycleManager: FlutterLifecycleManager(qoraClient: qoraClient),
connectivityManager: FlutterConnectivityManager(),
child: const MyApp(),
),
),
);
Utiliser les providers Riverpod pour injecter UserApi, QoraClient ou les repositories, puis les consommer depuis useQuery ou QoraBuilder.
Comparaison des fonctionnalités
| Fonctionnalité | FutureProvider | useQuery / QoraBuilder |
|---|---|---|
| SWR: servir périmé, refetch en arrière-plan | Non | Oui |
previousData pendant le refetch | Non | Oui |
| Retry automatique avec backoff | Non | Oui |
| Refetch en arrière-plan au focus / reconnexion | Non | Oui |
FetchStatus.paused (indicateur hors ligne) | Non | Oui |
| Persistance hors ligne | Non | Oui |
| Requêtes infinies / paginées | Non | Oui |
| Déduplication des requêtes en cours | Oui | Oui |
Checklist
- Ajouter
qora_flutteretqora_hooksàpubspec.yaml - Imbriquer
QoraScopeà l'intérieur deProviderScope - Remplacer
FutureProvider.familyparuseQuery(hooks) ouQoraBuilder(widgets) - Remplacer les méthodes de fetch de données de
AsyncNotifierparuseMutationouQoraMutationBuilder - Conserver
NotifierProviderde Riverpod pour l'état client, l'injection de dépendances et les graphes de services - Remplacer
ref.invalidate(provider)parclient.invalidate(key)ouclient.invalidateWhere