Prefetch au Niveau des Routes
Le prefetch au niveau des routes garantit que les données sont déjà en cache au moment où l'utilisateur navigue vers un nouvel écran. Ce guide couvre le prefetch par redirection GoRouter, par résolveur AutoRoute, les stratégies de gestion d'erreurs et les compromis entre cache-first et network-first.
API principale
Qora fournit trois primitives de prefetch, de la plus déclarative à la plus impérative :
| Primitive | Quand l'utiliser |
|---|---|
QoraPrefetch<T> (widget) | Envelopper un enfant déjà dans l'arbre (survol, focus, défilement). |
context.prefetch<T>() (extension) | Déclenchement impératif depuis un callback de geste ou événement de route. |
client.prefetch<T>() (cœur) | Tout contexte Dart sans arbre de widgets (gardes de route, résolveurs). |
Les trois sont sans effet quand des données fraîches existent déjà pour la queryKey donnée. Le prefetch s'exécute une fois par clé ; les appels répétés sont sûrs et peu coûteux.
// Widget : envelopper un enfant qui implique "susceptible de naviguer"
MouseRegion(
onEnter: (_) => setState(() => _prefetch = true),
child: _prefetch
? QoraPrefetch<User>(
queryKey: ['user', userId],
fetcher: () => api.getUser(userId),
child: UserListTile(userId: userId),
)
: UserListTile(userId: userId),
)
// Impératif : appeler depuis n'importe quel callback
GestureDetector(
onLongPress: () => context.prefetch<User>(
key: ['user', userId],
fetcher: () => api.getUser(userId),
),
child: UserTile(userId),
)
GoRouter : Prefetch par Redirection
GoRouter prend en charge les callbacks redirect qui s'exécutent avant qu'une route ne soit activée. Utilisez-les pour précharger les données de manière synchrone (fire-and-forget), puis laissez la garde de route continuer immédiatement. Lorsque le widget d'écran monte, son QoraBuilder lit depuis le cache sans attente réseau.
redirect s'exécute de manière synchrone. Le prefetch est de type fire-and-forget : ne pasawait le prefetch, ou vous bloquerez la transition de navigation.Implémentation
import 'package:go_router/go_router.dart';
import 'package:qora_flutter/qora_flutter.dart';
final _client = QoraClient();
final appRouter = GoRouter(
routes: [
GoRoute(
path: '/users',
builder: (_, __) => const UserListScreen(),
routes: [
GoRoute(
path: ':id',
redirect: (context, state) {
// Précharger le détail utilisateur pendant que la liste est visible.
final userId = state.pathParameters['id']!;
_client.prefetch<User>(
key: ['user', userId],
fetcher: () => api.getUser(userId),
).ignore(); // fire-and-forget
return null; // continuer vers la route
},
builder: (_, state) {
final userId = state.pathParameters['id']!;
return UserDetailScreen(userId: userId);
},
),
],
),
],
);
Avec gestion d'erreurs
Les échecs de prefetch ne doivent jamais bloquer la navigation. Enveloppez le fetcher dans un try/catch et logguez les erreurs pour l'observabilité :
redirect: (context, state) {
final userId = state.pathParameters['id']!;
_client.prefetch<User>(
key: ['user', userId],
fetcher: () async {
try {
return await api.getUser(userId);
} catch (e, s) {
log('Échec du prefetch pour user $userId', error: e, stackTrace: s);
rethrow; // toujours capturé par l'état Failure de Qora
}
},
).ignore();
return null;
},
AutoRoute : Prefetch par Résolveur
AutoRoute utilise RouteGuard ou un callback onNavigation dans le délégué du routeur. Une approche par résolveur déclenche le prefetch avant que le résolveur ne retourne la route. La route s'affiche seulement après que le prefetch est envoyé :
import 'package:auto_route/auto_route.dart';
import 'package:qora_flutter/qora_flutter.dart';
final _client = QoraClient();
@AutoRouterConfig()
class AppRouter extends RootStackRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(page: UserListRoute.page, path: '/users', children: [
AutoRoute(
page: UserDetailRoute.page,
path: ':id',
guards: [PrefetchGuard()],
),
]),
];
}
class PrefetchGuard extends AutoRouteGuard {
@override
Future<void> onNavigation(
NavigationResolver resolver,
StackRouter router,
) async {
final userId = resolver.route.pathParams.getString('id');
if (userId != null) {
_client
.prefetch<User>(
key: ['user', userId],
fetcher: () => api.getUser(userId),
)
.ignore();
}
// Ne jamais bloquer : résoudre immédiatement, les données chargent en arrière-plan.
resolver.next();
}
}
Avec un délai de grâce
Si vous voulez attendre un court délai (ex. 200 ms) pour donner une avance au prefetch avant de résoudre, sans jamais bloquer plus longtemps :
class PrefetchGuard extends AutoRouteGuard {
@override
Future<void> onNavigation(
NavigationResolver resolver,
StackRouter router,
) async {
final userId = resolver.route.pathParams.getString('id');
if (userId != null) {
final prefetch = _client.prefetch<User>(
key: ['user', userId],
fetcher: () => api.getUser(userId),
);
// Attendre au maximum 200 ms que le prefetch se termine.
try {
await prefetch.timeout(const Duration(milliseconds: 200));
} catch (_) {
// Timeout: le résolveur continue, les données chargent à l'écran.
}
}
resolver.next();
}
}
Stratégie : Cache-First vs Network-First
| Stratégie | Comportement | Quand l'utiliser |
|---|---|---|
| Cache-first (défaut) | Le prefetch est sans effet si des données fraîches existent. La première navigation après expiration déclenche un fetch. | La fenêtre de fraîcheur des données est connue et acceptable. |
| Network-first | Toujours fetcher à la navigation, en ignorant le contrôle de péremption du cache. | Les données doivent être garanties fraîches à chaque visite (ex. tableaux de bord financiers). |
Cache-first (par défaut)
La méthode client.prefetch<T>() implémente déjà le cache-first : elle vérifie le cache et retourne immédiatement si des données fraîches existent. Aucune configuration supplémentaire nécessaire.
Network-first
Forcer un fetch en appelant fetchQuery directement ou en invalidant la clé avant le prefetch :
redirect: (context, state) {
final userId = state.pathParameters['id']!;
// Toujours fetcher, même si des données fraîches existent.
_client.invalidate(['user', userId]);
_client.fetchQuery<User>(
key: ['user', userId],
fetcher: () => api.getUser(userId),
).ignore();
return null;
},
Pour un pattern réutilisable, définissez staleTime à Duration.zero dans les options de requête pour que chaque montage de QoraBuilder déclenche un fetch frais :
QoraBuilder<User>(
queryKey: ['user', userId],
fetcher: () => api.getUser(userId),
options: const QoraOptions(staleTime: Duration.zero),
builder: (context, state, _) { ... },
)
Utiliser QoraPrefetch dans une Route
Si vous ne pouvez pas (ou préférez ne pas) modifier votre couche de routage, intégrez QoraPrefetch au niveau du widget. C'est l'approche la plus simple quand l'enfant navigable est déjà dans l'arbre :
ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return QoraPrefetch<User>(
queryKey: ['user', user.id],
fetcher: () => api.getUser(user.id),
child: ListTile(
title: Text(user.name),
onTap: () => context.push('/users/${user.id}'),
),
);
},
)
Le prefetch se déclenche au montage (quand l'élément de liste apparaît) et se redéclenche si queryKey change. Au moment où l'utilisateur tape, les données sont en cache.
Tester la Logique de Prefetch
Utilisez QoraClient directement dans les tests sans aucun framework de routage :
import 'package:qora/qora.dart';
import 'package:test/test.dart';
void main() {
test('le prefetch remplit le cache avant le montage de l\'écran', () async {
final client = QoraClient();
await client.prefetch<User>(
key: ['user', 1],
fetcher: () async => User(id: 1, name: 'Alice'),
);
// Le widget d'écran monte maintenant et lit depuis le cache.
final state = client.getQueryState<User>(['user', 1]);
expect(state, isA<Success<User>>());
expect((state as Success<User>).data.name, 'Alice');
});
}
Étapes Suivantes
- QoraClient.prefetch: Référence API pour la méthode de prefetch cœur
- Bonnes Pratiques: Patterns de prefetch supplémentaires (survol, focus, défilement)
- Requêtes Dépendantes: Chaîner des requêtes qui dépendent de données prefetchées
- Annulation: Annuler les prefetchs en vol lors de navigations rapides
- Tests: Écrire des tests déterministes pour la logique de prefetch