fasq_riverpod 0.2.4
fasq_riverpod: ^0.2.4 copied to clipboard
Riverpod adapter for FASQ (Flutter Async State Query) - async state management with Riverpod
fasq_riverpod #
Riverpod adapter for FASQ (Flutter Async State Query) - bringing powerful async state management to your Riverpod-based Flutter apps.
Features #
- π queryProvider - Provider factory for queries
- βΎοΈ infiniteQueryProvider - Provider factory for infinite queries
- π QueryNotifier - StateNotifier for query state
- π combineQueries2/3 - Combine multiple query providers
- π Automatic caching - Built on FASQ's production-ready cache
- β‘ Background refetching - Stale-while-revalidate pattern
- π― Type-safe - Compile-time safety with Riverpod
Installation #
dependencies:
fasq_riverpod: ^0.1.0
Usage #
Infinite Queries with infiniteQueryProvider #
final postsProvider = infiniteQueryProvider<List<Post>, int>(
'posts',
(page) => api.fetchPosts(page: page),
options: InfiniteQueryOptions(
getNextPageParam: (pages, last) => pages.length + 1,
),
);
class Posts extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(postsProvider);
final notifier = ref.read(postsProvider.notifier);
return Column(
children: [
Expanded(child: ListView(/* render pages */)),
if (state.hasNextPage)
ElevatedButton(
onPressed: () => notifier.fetchNextPage(),
child: Text('Load More'),
),
],
);
}
}
Parallel Queries with combineQueries #
Execute multiple queries in parallel using combineQueries or combineNamedQueries:
// Index-based access
final usersProvider =
queryProvider('users'.toQueryKey(), () => api.fetchUsers());
final postsProvider =
queryProvider('posts'.toQueryKey(), () => api.fetchPosts());
final commentsProvider =
queryProvider('comments'.toQueryKey(), () => api.fetchComments());
final dashboardProvider = combineQueries([usersProvider, postsProvider, commentsProvider]);
class Dashboard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final combined = ref.watch(dashboardProvider);
return Column(
children: [
if (!combined.isAllSuccess) LinearProgressIndicator(),
if (combined.hasAnyError) ErrorBanner(),
UsersList(combined.getState<List<User>>(0)),
PostsList(combined.getState<List<Post>>(1)),
CommentsList(combined.getState<List<Comment>>(2)),
],
);
}
}
// Named access (better DX)
final dashboardProvider = combineNamedQueries({
'users': usersProvider,
'posts': postsProvider,
'comments': commentsProvider,
});
class Dashboard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final combined = ref.watch(dashboardProvider);
return Column(
children: [
if (!combined.isAllSuccess) LinearProgressIndicator(),
if (combined.hasAnyError) ErrorBanner(),
UsersList(combined.getState<List<User>>('users')),
PostsList(combined.getState<List<Post>>('posts')),
CommentsList(combined.getState<List<Comment>>('comments')),
],
);
}
}
Prefetching #
Warm the cache before data is needed:
// Using extension on WidgetRef
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Prefetch on hover
onHover: () => ref.prefetchQuery('user-123', () => api.fetchUser('123'));
}
}
// Using usePrefetch helper
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
usePrefetch(ref, [
PrefetchConfig(
queryKey: 'users'.toQueryKey(),
queryFn: () => api.fetchUsers(),
),
PrefetchConfig(
queryKey: 'posts'.toQueryKey(),
queryFn: () => api.fetchPosts(),
),
]);
return YourWidget();
}
}
Dependent Queries #
final userProvider = queryProvider<User>('user', () => fetchUser());
final postsProvider = queryProvider<List<Post>>(
'posts:user',
() => fetchPosts(ref.read(userProvider).data!.id),
options: QueryOptions(enabled: ref.read(userProvider).isSuccess),
);
Basic Query with queryProvider #
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fasq_riverpod/fasq_riverpod.dart';
final usersProvider = queryProvider<List<User>>(
'users',
() => api.fetchUsers(),
options: QueryOptions(
staleTime: Duration(minutes: 5),
),
);
class UsersScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersState = ref.watch(usersProvider);
if (usersState.isLoading) {
return CircularProgressIndicator();
}
if (usersState.hasError) {
return Text('Error: ${usersState.error}');
}
if (usersState.hasData) {
return UserList(users: usersState.data!);
}
return SizedBox();
}
}
Manual Refetch #
class UserList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: [
ElevatedButton(
onPressed: () {
// Refetch the query
ref.read(usersProvider.notifier).refetch();
},
child: Text('Refresh'),
),
// ... list content
],
);
}
}
Parameterized Queries with Family #
final userProvider = queryProvider.family<User, String>(
(id) => 'user:$id',
(id) => api.fetchUser(id),
options: QueryOptions(
staleTime: Duration(minutes: 5),
),
);
class UserProfile extends ConsumerWidget {
final String userId;
const UserProfile({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userProvider(userId));
if (userState.hasData) {
return Text('User: ${userState.data!.name}');
}
return CircularProgressIndicator();
}
}
Cache Invalidation #
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// Invalidate specific query
ref.read(usersProvider.notifier).invalidate();
// Or use QueryClient directly
QueryClient().invalidateQuery('users');
QueryClient().invalidateQueriesWithPrefix('user:');
},
child: Text('Invalidate Cache'),
);
}
}
Mutations #
For creating, updating, or deleting data:
final createUserProvider = mutationProvider<User, String>(
(name) => api.createUser(name),
options: MutationOptions(
onSuccess: (user) {
// Invalidate users query to refetch
QueryClient().invalidateQuery('users');
},
onError: (error) {
print('Error creating user: $error');
},
),
);
class CreateUserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final mutation = ref.watch(createUserProvider);
return Column(
children: [
ElevatedButton(
onPressed: mutation.isLoading
? null
: () => ref.read(createUserProvider.notifier).mutate('John Doe'),
child: mutation.isLoading
? CircularProgressIndicator()
: Text('Create User'),
),
if (mutation.hasError)
Text('Error: ${mutation.error}', style: TextStyle(color: Colors.red)),
if (mutation.hasData)
Text('Created: ${mutation.data!.name}'),
],
);
}
}
Form Submission #
class CreateUserForm extends ConsumerStatefulWidget {
@override
ConsumerState<CreateUserForm> createState() => _CreateUserFormState();
}
class _CreateUserFormState extends ConsumerState<CreateUserForm> {
final _nameController = TextEditingController();
final _emailController = TextEditingController();
late final createUserProvider;
@override
void initState() {
super.initState();
createUserProvider = mutationProvider<User, Map<String, String>>(
(data) => api.createUser(data['name']!, data['email']!),
options: MutationOptions(
onSuccess: (user) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('User created: ${user.name}')),
);
_nameController.clear();
_emailController.clear();
},
),
);
}
@override
Widget build(BuildContext context) {
final mutation = ref.watch(createUserProvider);
return Column(
children: [
TextField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Name'),
),
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
),
ElevatedButton(
onPressed: mutation.isLoading
? null
: () {
ref.read(createUserProvider.notifier).mutate({
'name': _nameController.text,
'email': _emailController.text,
});
},
child: mutation.isLoading
? CircularProgressIndicator()
: Text('Create User'),
),
],
);
}
}
Background Refetch Indicator #
class UsersScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersState = ref.watch(usersProvider);
return Column(
children: [
if (usersState.isFetching)
LinearProgressIndicator(), // Background refresh indicator
if (usersState.hasData)
Expanded(
child: UserList(users: usersState.data!),
),
],
);
}
}
API Reference #
queryProvider #
StateNotifierProvider<QueryNotifier<T>, QueryState<T>> queryProvider<T>(
String key,
Future<T> Function() queryFn, {
QueryOptions? options,
})
Parameters:
key- Unique identifier for the queryqueryFn- Async function that fetches the dataoptions- Optional configuration (staleTime, cacheTime, etc.)
Returns: StateNotifierProvider with QueryState<T>
QueryNotifier #
class QueryNotifier<T> extends StateNotifier<QueryState<T>> {
void refetch(); // Manually refetch
void invalidate(); // Invalidate and refetch
}
State: QueryState<T> with:
isLoading- Initial loading stateisFetching- Background refetch in progresshasData- Whether data is availabledata- The fetched datahasError- Whether an error occurrederror- The error object
Why Riverpod? #
If you're already using flutter_riverpod, this adapter provides seamless integration:
- Compile-safe - Type errors caught at compile time
- No context - Access providers anywhere
- Auto-dispose - Automatic cleanup
- DevTools - Riverpod DevTools integration
- Family - Parameterized queries
Comparison with Core Package #
Core Package (QueryBuilder):
QueryBuilder<List<User>>(
queryKey: 'users',
queryFn: () => api.fetchUsers(),
builder: (context, state) {
if (state.isLoading) return Loading();
return UserList(state.data!);
},
)
Riverpod Adapter (queryProvider):
final usersProvider = queryProvider('users', () => api.fetchUsers());
class UsersScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(usersProvider);
if (state.isLoading) return Loading();
return UserList(state.data!);
}
}
Both approaches use the same underlying query engine and have identical performance.
Advanced Usage #
Combining Multiple Queries #
class Dashboard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final users = ref.watch(usersProvider);
final posts = ref.watch(postsProvider);
final stats = ref.watch(statsProvider);
return Column(
children: [
UserSection(users),
PostSection(posts),
StatsSection(stats),
],
);
}
}
Conditional Queries #
final conditionalQueryProvider = queryProvider<Data>(
'conditional',
() => api.fetchData(),
options: QueryOptions(
enabled: someCondition, // Only fetches when true
),
);
Using ref.listen for Side Effects #
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(usersProvider, (previous, next) {
if (next.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${next.error}')),
);
}
});
// Build UI
}
}
Security Features π #
fasq_riverpod supports all FASQ security features through QueryClient configuration:
Secure Queries with queryProvider #
final secureTokenProvider = queryProvider<String>(
'auth-token',
() => api.getAuthToken(),
options: QueryOptions(
isSecure: true, // Mark as secure
maxAge: Duration(minutes: 15), // Required TTL
staleTime: Duration(minutes: 5),
),
client: context.queryClient, // Use configured client
);
class SecureTokenWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(secureTokenProvider);
if (state.isLoading) return CircularProgressIndicator();
if (state.hasError) return Text('Error: ${state.error}');
// Secure data never persisted, cleared on app background
return Text('Token: ${state.data}');
}
}
Secure Mutations with mutationProvider #
final secureMutationProvider = mutationProvider<String, String>(
(data) => api.secureMutation(data),
options: MutationOptions(
queueWhenOffline: true,
maxRetries: 3,
),
client: context.queryClient, // Use configured client
);
class SecureMutationWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final mutation = ref.watch(secureMutationProvider);
return ElevatedButton(
onPressed: mutation.isLoading
? null
: () => ref.read(secureMutationProvider.notifier).mutate('secure-data'),
child: mutation.isLoading
? CircularProgressIndicator()
: Text('Secure Mutation'),
);
}
}
Global Security Configuration #
QueryClientProvider(
config: CacheConfig(
defaultStaleTime: Duration(minutes: 5),
defaultCacheTime: Duration(minutes: 10),
),
persistenceOptions: PersistenceOptions(
enabled: true,
encryptionKey: 'your-encryption-key',
),
child: MaterialApp(
home: MyApp(),
),
)
Security Benefits:
- β Secure cache entries with automatic cleanup
- β Encrypted persistence for sensitive data
- β Input validation preventing injection attacks
- β Platform-specific secure key storage
Learn More #
- FASQ Documentation
- Riverpod Documentation
- React Query (inspiration)
License #
MIT