Guides Pratiques

Prefetch au Niveau des Routes

[object Object]

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 :

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

lib/features/user_detail_screen.dart
// 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.

Le callback 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

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

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

lib/app_router.dart
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 :

lib/guards/prefetch_guard.dart
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égieComportementQuand 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-firstToujours 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 :

lib/app_router.dart
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 :

lib/features/user_detail_screen.dart
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 :

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

test/user_prefetch_test.dart
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
Copyright © 2026