volt ⚡️

Effortlessly manage asynchronous data fetching, caching, and real-time data delivery with minimal code.

Features

  • ⚡️ Minimal boilerplate code for faster development
  • 📡 Reactive updates for data consistency between components
  • 🚀 In-memory caching for instant data access
  • 💾 Disk caching with offline support
  • 🔄 Query deduplication to reduce network requests
  • 🔮 Configurable auto-refetching for fresh data
  • 🧩 Easy integration with Flutter projects
  • 🧠 Compute isolate support for heavy operations
  • 📦 Lightweight package with minimal dependencies
  • 🔒 Error handling with automatic retry mechanisms

Install

flutter pub add volt

Usage

Query

VoltQuery<Photo> photoQuery(String id) => VoltQuery(
      queryKey: ['photo', id],
      queryFn: () => fetch('https://jsonplaceholder.typicode.com/photos/$id'),
      select: Photo.fromJson,
    );

Widget build(BuildContext context) {
  final photo = useQuery(photoQuery('1'));

  return photo == null ? CircularProgressIndicator() : Text('Photo: ${photo.title}');
}

Mutation

VoltMutation<String> useDeletePhotoMutation() {
  final queryClient = useQueryClient();

  return useMutation(
    mutationFn: (photoId) => fetch(
      'https://jsonplaceholder.typicode.com/photos/$photoId',
      method: 'DELETE',
    ),
    onSuccess: (photoId) => queryClient.prefetchQuery(photoQuery(photoId)),
  );
}

Widget build(BuildContext context) {
  final deletePhotoMutation = useDeletePhotoMutation();

  return deletePhotoMutation.state.isLoading
      ? const CircularProgressIndicator()
      : ElevatedButton(
          onPressed: () => deletePhotoMutation.mutate('1'),
          child: const Text('Delete Photo'),
        );
}

Configuration

Widget build(BuildContext context) {
  final queryClient = useMemoized(() => QueryClient(
    // Transforms query keys (useful for cache segmentation by environment/locale)
    keyTransformer: (keys) => keys,

    // Custom persistor for memory and disk caching
    persistor: FileVoltPersistor(),

    // Global default stale duration
    staleDuration: const Duration(hours: 1),

    // Enable debug mode for extra logging and stats
    isDebug: false,

    // Listener for query events (cache hits, network errors, etc.)
    listener: null,
  ));

  return QueryClientProvider(
    client: queryClient,
    child: MyApp(),
  );
}

Query dependencies

A null queryFn acts the same as enabled: false

final accountQuery = VoltQuery(
  queryKey: ['account'],
  queryFn: () async => fetch('https://jsonplaceholder.typicode.com/account/1'),
  select: Account.fromJson,
);

VoltQuery<Photos> photosQuery(Account? account) =>
    VoltQuery(
      queryKey: ['photos', account?.id],
      queryFn: account == null
          ? null
          : () async => fetch('https://jsonplaceholder.typicode.com/account/${account.id}/photos/'),
      select: Photos.fromJson,
    );

Widget build(BuildContext context) {
  final account = useQuery(accountQuery);
  final photos = useQuery(photosQuery(account));

  ...
}

Best Practices

Response Object Equality

Response objects should implement equality to ensure proper change detection and prevent unnecessary rebuilds. Use equatable, freezed, or implement equality manually.

Query Key Structure

Use consistent, hierarchical query keys. Start with a general identifier and add specifics:

// Good
['users']
['users', userId]
['users', userId, 'posts']
['users', userId, 'posts', postId]

// Avoid
['getUser123']
[userId, 'users']  // inconsistent order

Extract Query Definitions

Define queries as functions outside widgets for reusability and testability:

// Good - reusable across the app
VoltQuery<User> userQuery(String id) => VoltQuery(
  queryKey: ['user', id],
  queryFn: () => fetchUser(id),
  select: User.fromJson,
);

// Avoid - inline queries are harder to reuse
useQuery(VoltQuery(queryKey: ['user', id], ...));

Cache segmentation

Use keyTransformer in QueryClient to automatically segment cache by environment (production, staging, etc.)/locale (en, es, etc.) for all queries:

final queryClient = QueryClient(
  keyTransformer: (keys) => [
    isProduction ? 'production' : 'staging',
    locale,
    ...keys
    ],
);

This ensures cache isolation between environments and prevents data conflicts.

Persistence

By default, Volt persists data to disk using the FileVoltPersistor. Which relies on no heavy dependencies and is very fast (uses the file system). Although, this can be overridden with a custom persister in the QueryClient constructor.

Credits

Volt's public API design was inspired by React Query, a popular data-fetching and state management library for React applications.

Special thanks to flutter_hooks for bringing React-style hooks to Flutter, which made this package possible.

Libraries

volt