fluquery 1.0.4 copy "fluquery: ^1.0.4" to clipboard
fluquery: ^1.0.4 copied to clipboard

Async state management & data fetching for Flutter. Caching, mutations, infinite queries, optimistic updates.

FluQuery πŸš€ #

Powerful asynchronous state management for Flutter - Inspired by TanStack Query

FluQuery makes fetching, caching, synchronizing, and updating server state in your Flutter applications a breeze. Say goodbye to boilerplate code and complex state management!

pub package License: MIT

✨ Features #

  • πŸ”„ Automatic Caching - Data is cached automatically with configurable stale times
  • πŸ” Background Refetching - Stale data is automatically refreshed in the background
  • πŸ“± Window Focus Refetching - Automatically refetch when app comes to foreground (mobile) or tab gains focus (web)
  • 🌐 Network Reconnection Handling - Refetch when network reconnects
  • ⏱️ Polling/Realtime Updates - Built-in interval-based refetching
  • πŸ“„ Infinite Queries - Cursor-based pagination made easy
  • ✏️ Mutations - Create, update, delete with automatic cache invalidation
  • ⚑ Optimistic Updates - Instant UI updates with automatic rollback on error
  • πŸ”— Dependent Queries - Sequential queries that depend on each other
  • πŸ”„ Parallel Queries - Run multiple queries simultaneously
  • 🏎️ Race Condition Handling - Automatic cancellation of stale requests
  • 🎯 Retry Logic - Automatic retries with exponential backoff
  • 🧹 Garbage Collection - Automatic cleanup of unused cache entries
  • πŸͺ Hooks API - Beautiful Flutter Hooks integration
  • πŸ” Select/Transform - Transform query data before returning (useQuerySelect)
  • πŸ“ Keep Previous Data - Smooth transitions between queries with keepPreviousData

πŸ“¦ Installation #

Add FluQuery to your pubspec.yaml:

dependencies:
  fluquery: ^1.0.0
  flutter_hooks: ^0.20.5

πŸš€ Quick Start #

1. Setup QueryClientProvider #

Wrap your app with QueryClientProvider:

import 'package:fluquery/fluquery.dart';

void main() {
  runApp(
    QueryClientProvider(
      client: QueryClient(),
      child: MyApp(),
    ),
  );
}

2. Use Queries with Hooks #

class TodoList extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final todos = useQuery<List<Todo>, Object>(
      queryKey: ['todos'],
      queryFn: (_) => fetchTodos(),
    );

    if (todos.isLoading) {
      return CircularProgressIndicator();
    }

    if (todos.isError) {
      return Text('Error: ${todos.error}');
    }

    return ListView(
      children: todos.data!.map((t) => TodoItem(todo: t)).toList(),
    );
  }
}

πŸ“– Usage #

Basic Query #

final query = useQuery<User, Object>(
  queryKey: ['user', userId],
  queryFn: (_) => fetchUser(userId),
  staleTime: const StaleTime(Duration(minutes: 5)),
);

// Access data
if (query.isSuccess) {
  print(query.data);
}

// Refetch manually
query.refetch();

Query with Options #

final query = useQuery<List<Post>, Object>(
  queryKey: ['posts'],
  queryFn: (_) => fetchPosts(),
  
  // Time after which data is considered stale
  staleTime: const StaleTime(Duration(minutes: 5)),
  
  // Garbage collection time (how long inactive data stays in cache)
  gcTime: const GcTime(Duration(minutes: 10)),
  
  // Polling interval
  refetchInterval: Duration(seconds: 30),
  
  // Retry configuration
  retry: 3,
  retryDelay: (attempt, error) => Duration(seconds: attempt * 2),
  
  // Conditional fetching
  enabled: isLoggedIn,
  
  // Refetch behavior
  refetchOnMount: true,        // Refetch when widget mounts (if stale)
  refetchOnWindowFocus: true,  // Refetch when app/tab gains focus
  refetchOnReconnect: true,    // Refetch when network reconnects
  
  // Initial/placeholder data
  placeholderData: [],
  initialData: cachedPosts,
);

Mutations #

class CreateTodo extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final client = useQueryClient();
    
    final mutation = useMutation<Todo, Object, String, void>(
      mutationFn: (title) => createTodo(title),
      onSuccess: (data, variables, _) {
        // Invalidate and refetch todos
        client.invalidateQueries(queryKey: ['todos'], refetchType: true);
      },
    );

    return ElevatedButton(
      onPressed: mutation.isPending 
        ? null 
        : () => mutation.mutate('New Todo'),
      child: mutation.isPending 
        ? CircularProgressIndicator()
        : Text('Add Todo'),
    );
  }
}

Optimistic Updates #

final toggleMutation = useMutation<Todo, Object, Todo, List<Todo>>(
  mutationFn: (todo) => updateTodo(todo.id, completed: !todo.completed),
  
  onMutate: (todo) {
    // Cancel outgoing refetches
    client.cancelQueries(queryKey: ['todos']);
    
    // Snapshot previous value
    final previousTodos = client.getQueryData<List<Todo>>(['todos']);
    
    // Optimistically update
    if (previousTodos != null) {
      final newTodos = previousTodos.map((t) {
        return t.id == todo.id ? t.copyWith(completed: !t.completed) : t;
      }).toList();
      client.setQueryData(['todos'], newTodos);
    }
    
    return previousTodos ?? [];
  },
  
  onError: (error, todo, previousTodos) {
    // Rollback on error
    if (previousTodos != null) {
      client.setQueryData(['todos'], previousTodos);
    }
  },
  
  onSettled: (_, __, ___, ____) {
    // Refetch after mutation
    client.invalidateQueries(queryKey: ['todos'], refetchType: true);
  },
);

Race Condition Handling #

FluQuery automatically handles race conditions. When a user types quickly in a search field, earlier (slower) requests won't override later (faster) results:

final searchQuery = useQuery<List<User>, Object>(
  // Query key includes the search term - each unique term is a separate query
  queryKey: ['users', 'search', searchTerm],
  queryFn: (ctx) async {
    // Check for cancellation periodically in long operations
    if (ctx.signal?.isCancelled == true) {
      throw QueryCancelledException();
    }
    
    return await searchUsers(searchTerm);
  },
  enabled: searchTerm.isNotEmpty,
);

// Manually cancel previous queries when search term changes
void onSearchChanged(String newTerm) {
  // Cancel the previous search query
  client.cancelQueries(queryKey: ['users', 'search', previousTerm]);
  previousTerm = newTerm;
}

Infinite Queries #

final postsQuery = useInfiniteQuery<PostsPage, Object, int>(
  queryKey: ['posts'],
  queryFn: (ctx) => fetchPosts(page: ctx.pageParam ?? 1),
  initialPageParam: 1,
  getNextPageParam: (lastPage, allPages, lastParam, allParams) {
    return lastPage.hasMore ? lastPage.nextPage : null;
  },
);

// Load more
if (postsQuery.hasNextPage && !postsQuery.isFetchingNextPage) {
  postsQuery.fetchNextPage();
}

// Access all pages
final allPosts = postsQuery.pages.expand((page) => page.posts).toList();

Dependent Queries #

// First query
final userQuery = useQuery<User, Object>(
  queryKey: ['user', userId],
  queryFn: (_) => fetchUser(userId),
);

// Dependent query - only runs when user query succeeds
final postsQuery = useQuery<List<Post>, Object>(
  queryKey: ['user-posts', userId],
  queryFn: (_) => fetchUserPosts(userId),
  enabled: userQuery.isSuccess,  // Only fetch when user is loaded
);

Polling #

final timeQuery = useQuery<ServerTime, Object>(
  queryKey: ['server-time'],
  queryFn: (_) => fetchServerTime(),
  refetchInterval: Duration(seconds: 5),  // Poll every 5 seconds
);

Select (Data Transformation) #

Use useQuerySelect to transform data before returning. The raw data is still cached, but your component only receives the transformed result:

// Fetch all users but only return their names
final userNames = useQuerySelect<List<User>, Object, List<String>>(
  queryKey: ['users'],
  queryFn: (_) => fetchUsers(),
  select: (users) => users.map((u) => u.name).toList(),
);

// Result is List<String>, not List<User>!
print(userNames.data); // ['John', 'Jane', 'Bob']

// Compute derived values
final userCount = useQuerySelect<List<User>, Object, int>(
  queryKey: ['users'],
  queryFn: (_) => fetchUsers(),
  select: (users) => users.length,
);

print(userCount.data); // 42

Keep Previous Data #

Enable smooth transitions between queries by keeping previous data visible while fetching:

final userPosts = useQuery<List<Post>, Object>(
  queryKey: ['posts', userId],
  queryFn: (_) => fetchUserPosts(userId),
  keepPreviousData: true,  // Magic!
);

// When userId changes:
// 1. Previous posts stay visible (no loading spinner!)
// 2. New posts are fetched in background
// 3. UI smoothly updates when new data arrives

// Check if showing previous data
if (userPosts.isPreviousData) {
  showBadge('Updating...');
}

Parallel Queries #

final results = useQueries(
  queries: [
    QueryConfig(
      queryKey: ['users'],
      queryFn: (_) => fetchUsers(),
    ),
    QueryConfig(
      queryKey: ['posts'],
      queryFn: (_) => fetchPosts(),
    ),
    QueryConfig(
      queryKey: ['comments'],
      queryFn: (_) => fetchComments(),
    ),
  ],
);

// Access individual results
final usersResult = results[0];
final postsResult = results[1];
final commentsResult = results[2];

QueryBuilder Widget (Alternative to Hooks) #

QueryBuilder<List<Todo>, Object>(
  queryKey: ['todos'],
  queryFn: (_) => fetchTodos(),
  builder: (context, result) {
    if (result.isLoading) return CircularProgressIndicator();
    if (result.isError) return Text('Error: ${result.error}');
    
    return ListView(
      children: result.data!.map((t) => TodoItem(todo: t)).toList(),
    );
  },
)

βš™οΈ Configuration #

QueryClient Options #

final client = QueryClient(
  config: QueryClientConfig(
    defaultOptions: DefaultQueryOptions(
      staleTime: StaleTime(Duration(minutes: 5)),
      gcTime: GcTime(Duration(minutes: 10)),
      retry: 3,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
      refetchOnMount: true,
    ),
    logLevel: LogLevel.debug,  // Set to LogLevel.warn for production
  ),
);

Query Keys #

Query keys are used for caching and deduplication. They can be strings or arrays:

// Simple key
queryKey: ['todos']

// With variables - each unique combination is a separate cache entry
queryKey: ['todo', todoId]

// Complex keys
queryKey: ['user', userId, 'posts', { 'status': 'active' }]

🎯 API Reference #

Hooks #

Hook Description
useQuery Fetch and cache data
useQuerySelect Fetch with data transformation
useMutation Create/update/delete operations
useInfiniteQuery Paginated/infinite queries
useQueries Parallel queries
useQueryClient Access the QueryClient
useIsFetching Check if any queries are fetching
useIsMutating Check if any mutations are pending
useSimpleQuery Simplified query hook

QueryResult Properties #

Property Type Description
data T? The resolved data
error E? Any error that occurred
isLoading bool Initial load in progress
isFetching bool Any fetch in progress
isError bool Error state
isSuccess bool Success state
isRefetching bool Background refetch
isStale bool Data is stale
isPending bool Query hasn't run yet
hasData bool Data is available
isPreviousData bool Showing previous data (keepPreviousData)
isPlaceholderData bool Showing placeholder data
refetch Function Manually refetch
dataUpdatedAt DateTime? When data was last updated

QueryClient Methods #

Method Description
fetchQuery Fetch a query programmatically
prefetchQuery Prefetch a query
getQueryData Get cached data
setQueryData Set cached data directly
invalidateQueries Mark queries as stale and optionally refetch
refetchQueries Force refetch queries
cancelQueries Cancel in-flight queries
removeQueries Remove from cache
resetQueries Reset to initial state

MutationResult Properties #

Property Type Description
data T? The mutation result
error E? Any error that occurred
isPending bool Mutation in progress
isError bool Error state
isSuccess bool Success state
isIdle bool Not yet triggered
variables V? Current mutation variables
mutate Function Trigger the mutation
reset Function Reset mutation state

πŸ“± Example App #

Check out the example directory for a comprehensive demo app showcasing:

  • βœ… Basic queries with loading/error states
  • βœ… Mutations with cache invalidation
  • βœ… Infinite scroll pagination
  • βœ… Dependent/sequential queries
  • βœ… Polling/realtime updates
  • βœ… Optimistic updates with rollback
  • βœ… Race condition handling

Running the Example #

  1. Start the backend server:

    cd backend
    dart pub get
    dart run bin/server.dart
    

    The server runs at http://localhost:8080

  2. Run the Flutter app:

    cd example
    flutter pub get
    flutter run -d chrome  # or any other platform
    

πŸ—ΊοΈ Roadmap #

We're continuously improving FluQuery. Here's what's coming:

πŸ”₯ High Priority #

Feature Status Description
πŸ’Ύ Persister Plugin πŸ”œ Planned Save/restore cache to disk (Hive, SharedPrefs, SQLite)
πŸ”§ DevTools πŸ”œ Planned Debug overlay to inspect cache, queries, and mutations
πŸ“Š Max Cache Size πŸ”œ Planned Limit cache entries to prevent memory issues
πŸ›‘οΈ QueryErrorBoundary πŸ”œ Planned Widget for graceful error handling and recovery

⚑ Medium Priority #

Feature Status Description
πŸ“΄ Offline Mutation Queue πŸ“‹ Backlog Queue mutations when offline, execute on reconnect
πŸ“¦ Request Batching πŸ“‹ Backlog Combine multiple requests into one
πŸ”„ Structural Sharing πŸ“‹ Backlog Optimize re-renders with deep comparison
🎭 Suspense-like Boundary πŸ“‹ Backlog Loading boundary widget for child queries

πŸš€ Future #

Feature Status Description
πŸ”Œ WebSocket Integration πŸ’‘ Idea Real-time updates via WebSocket
πŸ“‘ GraphQL Adapter πŸ’‘ Idea First-class GraphQL support
πŸ” Auth Token Refresh πŸ’‘ Idea Automatic 401 handling with token refresh
βš–οΈ Optimistic Locking πŸ’‘ Idea Conflict resolution for concurrent updates

βœ… Completed Features #

  • βœ… Automatic caching & background refetching
  • βœ… Window focus & network reconnection handling
  • βœ… Mutations with cache invalidation
  • βœ… Infinite/paginated queries
  • βœ… Optimistic updates with rollback
  • βœ… Dependent & parallel queries
  • βœ… Race condition handling with CancellationToken
  • βœ… Select/Transform data (useQuerySelect)
  • βœ… Keep Previous Data for smooth transitions
  • βœ… Polling/interval refetching
  • βœ… Retry with exponential backoff
  • βœ… Garbage collection

πŸ’‘ Have a feature request? Open an issue!


🀝 Contributing #

Contributions are welcome! Please read our contributing guidelines before submitting a PR.

πŸ“„ License #

MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments #


Made with ❀️ for the Flutter community

7
likes
0
points
190
downloads

Publisher

unverified uploader

Weekly Downloads

Async state management & data fetching for Flutter. Caching, mutations, infinite queries, optimistic updates.

Repository (GitHub)
View/report issues

Topics

#state-management #caching #api #hooks #query

Documentation

Documentation

License

unknown (license)

Dependencies

collection, connectivity_plus, flutter, flutter_hooks

More

Packages that depend on fluquery