Third Party

With Riverpod

Add Qora to a Riverpod project to complement FutureProvider with SWR, offline behavior, and hooks-oriented query APIs.

Riverpod Provider and NotifierProvider are effective for dependency injection and client state. FutureProvider handles async reads but does not provide SWR, retry/backoff, or offline pause and replay semantics by default.

Qora adds these server-state capabilities without replacing Riverpod. Riverpod remains focused on dependency graphs and client state.


What FutureProvider Misses

lib/providers/user_provider.dart
final userProvider = FutureProvider.family<User, int>((ref, id) async {
  return UserApi.getUser(id);
});
lib/user_screen.dart
final userAsync = ref.watch(userProvider(userId));

return userAsync.when(
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => ErrorWidget('$e'),
  data: (user) => UserCard(user),
);

Limitations:

  • Full-screen spinner on every refresh: old data is discarded, no stale-while-revalidate.
  • No retry with backoff: a single failure is surfaced immediately.
  • No background refetch on app focus or reconnect.
  • No offline support: no FetchStatus, and no pause/replay behavior on reconnect.
  • AsyncValue has no previousData: no graceful degradation path.

Hooks Path

If you are already using flutter_hooks (which Riverpod developers often are via hooks_riverpod), qora_hooks provides a useQuery hook that feels native:

pubspec.yaml
dependencies:
  qora_flutter: ^ 1.3.1
  qora_hooks: ^ 1.3.1   # requires flutter_hooks
lib/user_screen.dart
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 returns QoraState<T>, the same sealed type used across Qora APIs. Stale-while-revalidate, retry, background refetch, and deduplication are built in.

Graceful Degradation with previousData

Loading and Failure carry previousData, which is the last successful value:

lib/user_screen.dart
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 with useMutation

lib/update_name_form.dart
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('Save'),
    );
  }
}

MutationHandle exposes: isIdle, isPending, isSuccess, isError, data, error, mutate(variables), mutateAsync(variables), reset().


Widget API Alternative

If you prefer to stay with StatefulWidget rather than HookWidget, QoraBuilder provides the same capabilities:

lib/user_screen.dart
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(),
    };
  },
)

The third builder argument fetchStatus exposes FetchStatus.paused in offline states. Use QoraBuilder when this signal is required by the UI.


Keeping Riverpod for DI and Client State

Riverpod's Provider and NotifierProvider are unmatched for dependency injection and client state. The two scopes compose cleanly:

lib/main.dart
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(),
    ),
  ),
);

Use Riverpod providers to inject UserApi, QoraClient, or repositories, then consume them from useQuery or QoraBuilder.


Feature Comparison

FeatureFutureProvideruseQuery / QoraBuilder
SWR: serve stale, refetch in backgroundNoYes
previousData during refetchNoYes
Auto retry with backoffNoYes
Background refetch on focus / reconnectNoYes
FetchStatus.paused (offline indicator)NoYes
Offline persistenceNoYes
Infinite / paginated queriesNoYes
Deduplication of in-flight requestsYesYes

Checklist

  • Add qora_flutter and qora_hooks to pubspec.yaml
  • Nest QoraScope inside ProviderScope
  • Replace FutureProvider.family with useQuery (hooks) or QoraBuilder (widgets)
  • Replace AsyncNotifier data-fetch methods with useMutation or QoraMutationBuilder
  • Keep Riverpod NotifierProvider for client state, DI, and service graphs
  • Replace ref.invalidate(provider) with client.invalidate(key) or client.invalidateWhere

Copyright © 2026