fluquery 1.0.0
fluquery: ^1.0.0 copied to clipboard
Powerful asynchronous state management, server-state utilities and data fetching for Flutter. Inspired by TanStack Query (React Query). Features automatic caching, background refetching, mutations, [...]
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!
โจ 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 #
-
Start the backend server:
cd backend dart pub get dart run bin/server.dartThe server runs at
http://localhost:8080 -
Run the Flutter app:
cd example flutter pub get flutter run -d chrome # or any other platform
๐ค Contributing #
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
๐ License #
MIT License - see the LICENSE file for details.
๐ Acknowledgments #
- Inspired by TanStack Query (React Query)
- Built with flutter_hooks
Made with โค๏ธ by the Flutter community