fasq_riverpod 0.2.3+1 copy "fasq_riverpod: ^0.2.3+1" to clipboard
fasq_riverpod: ^0.2.3+1 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 query
  • queryFn - Async function that fetches the data
  • options - 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 state
  • isFetching - Background refetch in progress
  • hasData - Whether data is available
  • data - The fetched data
  • hasError - Whether an error occurred
  • error - 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 #

License #

MIT

1
likes
0
points
82
downloads

Publisher

verified publishershafi.dev

Weekly Downloads

Riverpod adapter for FASQ (Flutter Async State Query) - async state management with Riverpod

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

fasq, flutter, flutter_riverpod

More

Packages that depend on fasq_riverpod