Prise en main

Démarrage rapide

Construire un écran liste et un écran détail avec Qora, voir le cache travailler, puis passer l'application hors-ligne.

En dix minutes, vous aurez deux écrans qui chargent des utilisateurs depuis une vraie API, s'ouvrent instantanément à la deuxième visite, se rafraîchissent en arrière-plan et continuent d'afficher les données quand le réseau tombe.

Le code ci-dessous fonctionne tel quel avec JSONPlaceholder, une fausse API REST gratuite.

1. Installer et envelopper l'application

Terminal
flutter pub add qora_flutter http
lib/main.dart
import 'package:flutter/material.dart';
import 'package:qora_flutter/qora_flutter.dart';

import 'user_list_screen.dart';

void main() {
  final client = QoraClient(
    config: const QoraClientConfig(
      defaultOptions: QoraOptions(staleTime: Duration(minutes: 5)),
    ),
  );

  runApp(QoraScope.auto(client: client, child: const MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) =>
      const MaterialApp(home: UserListScreen());
}

staleTime: 5 minutes signifie qu'une donnée récupérée reste fraîche cinq minutes : dans cette fenêtre, rouvrir un écran ne coûte aucun appel réseau.

2. Décrire les données et comment les récupérer

lib/user_api.dart
import 'dart:convert';

import 'package:http/http.dart' as http;

class User {
  final int id;
  final String name;
  final String email;

  const User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) => User(
        id: json['id'] as int,
        name: json['name'] as String,
        email: json['email'] as String,
      );
}

class UserApi {
  static const _base = 'https://jsonplaceholder.typicode.com';

  static Future<List<User>> getUsers() async {
    final res = await http.get(Uri.parse('$_base/users'));
    if (res.statusCode != 200) throw Exception('HTTP ${res.statusCode}');
    final list = jsonDecode(res.body) as List<dynamic>;
    return list.map((j) => User.fromJson(j as Map<String, dynamic>)).toList();
  }

  static Future<User> getUser(int id) async {
    final res = await http.get(Uri.parse('$_base/users/$id'));
    if (res.statusCode != 200) throw Exception('HTTP ${res.statusCode}');
    return User.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
  }
}
Un fetcher doit lever une exception en cas d'échec. S'il renvoie null ou avale l'erreur, Qora voit un succès : il ne relance jamais et n'affiche pas l'erreur.

3. L'écran liste

QoraBuilder récupère les données au montage, met le résultat en cache sous sa clé et se reconstruit à chaque changement. Le builder reçoit l'état, que vous matchez de façon exhaustive, et un FetchStatus qui indique si une requête est en cours.

lib/user_list_screen.dart
import 'package:flutter/material.dart';
import 'package:qora_flutter/qora_flutter.dart';

import 'user_api.dart';
import 'user_detail_screen.dart';

class UserListScreen extends StatelessWidget {
  const UserListScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Users')),
      body: QoraBuilder<List<User>>(
        queryKey: const ['users'],
        fetcher: UserApi.getUsers,
        builder: (context, state, fetchStatus) => switch (state) {
          Success(:final data) => _UserList(users: data),
          // Un refetch ou une erreur conserve les données précédentes : on les affiche.
          Loading(:final previousData?) => _UserList(users: previousData),
          Failure(:final previousData?) => _UserList(users: previousData),
          Failure(:final error) => Center(child: Text('Could not load users: $error')),
          _ => const Center(child: CircularProgressIndicator()),
        },
      ),
    );
  }
}

class _UserList extends StatelessWidget {
  const _UserList({required this.users});
  final List<User> users;

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      // Marque la liste comme périmée : le QoraBuilder monté la recharge.
      onRefresh: () async => context.qora.invalidate(const ['users']),
      child: ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, i) => ListTile(
          title: Text(users[i].name),
          subtitle: Text(users[i].email),
          onTap: () => Navigator.of(context).push(MaterialPageRoute(
            builder: (_) => UserDetailScreen(userId: users[i].id),
          )),
        ),
      ),
    );
  }
}

4. L'écran détail

Chaque utilisateur a sa propre clé, ['users', id]. La clé est l'adresse dans le cache : même clé, mêmes données, partout où elle est utilisée.

lib/user_detail_screen.dart
import 'package:flutter/material.dart';
import 'package:qora_flutter/qora_flutter.dart';

import 'user_api.dart';

class UserDetailScreen extends StatelessWidget {
  const UserDetailScreen({super.key, required this.userId});
  final int userId;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User #$userId')),
      body: QoraBuilder<User>(
        queryKey: ['users', userId],
        fetcher: () => UserApi.getUser(userId),
        builder: (context, state, fetchStatus) {
          final user = state.dataOrNull; // conservé pendant les refetchs et les erreurs
          if (user == null) {
            return switch (state) {
              Failure(:final error) => Center(child: Text('Error: $error')),
              _ => const Center(child: CircularProgressIndicator()),
            };
          }
          return ListTile(title: Text(user.name), subtitle: Text(user.email));
        },
      ),
    );
  }
}

Lancez l'application. Ouvrez un utilisateur, revenez, rouvrez-le : la deuxième fois, il s'affiche instantanément, sans spinner. Il vient du cache.

5. Passer hors-ligne

QoraScope.auto suit déjà la connectivité. Quand l'appareil est hors-ligne, les requêtes n'échouent pas : elles se mettent en pause, et fetchStatus devient FetchStatus.paused. Affichez-le dans l'écran liste :

lib/user_list_screen.dart
builder: (context, state, fetchStatus) => Column(
  children: [
    if (fetchStatus == FetchStatus.paused)
      Container(
        width: double.infinity,
        padding: const EdgeInsets.all(8),
        color: Colors.amber.shade100,
        child: const Text('Vous êtes hors-ligne. Données sauvegardées affichées.'),
      ),
    Expanded(
      child: switch (state) {
        Success(:final data) => _UserList(users: data),
        Loading(:final previousData?) => _UserList(users: previousData),
        Failure(:final previousData?) => _UserList(users: previousData),
        Failure(:final error) => Center(child: Text('Could not load users: $error')),
        _ => const Center(child: CircularProgressIndicator()),
      },
    ),
  ],
),

Activez le mode avion et tirez pour rafraîchir : la liste reste, le bandeau apparaît. Réactivez le réseau : le fetch en pause s'exécute tout seul et le bandeau disparaît.

Ce que vous obtenez sans rien écrire

SituationCe que fait Qora
Première visiteLoading, puis Success ; le résultat est mis en cache sous sa clé
Visite dans les 5 minutesDonnées du cache dès la première frame, aucun appel réseau
Visite après 5 minutesDonnées du cache dès la première frame, rafraîchies en arrière-plan
Tirer pour rafraîchirinvalidate marque la clé comme périmée ; le builder monté recharge
Deux widgets, même cléUne seule requête, partagée
La requête échoue3 retries avec backoff, puis Failure avec les données précédentes
Appareil hors-ligneLes fetchs se mettent en pause et reprennent à la reconnexion ; les données restent affichées
Retour d'arrière-planLes requêtes périmées affichées à l'écran se relancent

Étapes suivantes

Copyright © 2026