fasq_bloc 0.2.2 copy "fasq_bloc: ^0.2.2" to clipboard
fasq_bloc: ^0.2.2 copied to clipboard

Bloc/Cubit adapter for FASQ (Flutter Async State Query) - async state management with Bloc

fasq_bloc #

Bloc/Cubit adapter for FASQ (Flutter Async State Query) - bringing powerful async state management to your Bloc-based Flutter apps.

Features #

  • 🧊 QueryCubit - Abstract base cubit for queries
  • ♾️ InfiniteQueryCubit - Abstract base cubit for infinite queries
  • πŸ”„ MutationCubit - Abstract base cubit for server mutations
  • πŸ”€ MultiQueryBuilder - Execute multiple queries in parallel
  • πŸš€ Automatic caching - Built on FASQ's production-ready cache
  • ⚑ Background refetching - Stale-while-revalidate pattern
  • 🎯 Type-safe - Full type safety with Bloc

Installation #

dependencies:
  fasq_bloc: ^0.1.0

Usage #

Basic Query with QueryCubit #

Extend QueryCubit and implement the required getters:

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fasq_bloc/fasq_bloc.dart';

class UsersQueryCubit extends QueryCubit<List<User>> {
  @override
  String get key => 'users';

  @override
  Future<List<User>> Function() get queryFn => () => api.fetchUsers();

  @override
  QueryOptions? get options => QueryOptions(
    staleTime: Duration(minutes: 5),
  );
}

class UsersScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => UsersQueryCubit(),
      child: BlocBuilder<UsersQueryCubit, QueryState<List<User>>>(
        builder: (context, state) {
          if (state.isLoading) {
            return CircularProgressIndicator();
          }
          
          if (state.hasError) {
            return Text('Error: ${state.error}');
          }
          
          if (state.hasData) {
            return UserList(users: state.data!);
          }
          
          return SizedBox();
        },
      ),
    );
  }
}

Infinite Queries with InfiniteQueryCubit #

Extend InfiniteQueryCubit for pagination:

class PostsInfiniteQueryCubit extends InfiniteQueryCubit<List<Post>, int> {
  @override
  String get key => 'posts';

  @override
  Future<List<Post>> Function(int param) get queryFn => 
    (page) => api.fetchPosts(page: page);

  @override
  InfiniteQueryOptions<List<Post>, int>? get options => InfiniteQueryOptions(
    getNextPageParam: (pages, last) => pages.length + 1,
  );
}

BlocProvider(
  create: (_) => PostsInfiniteQueryCubit(),
  child: BlocBuilder<PostsInfiniteQueryCubit, InfiniteQueryState<List<Post>, int>>(
    builder: (context, state) {
      final allPosts = state.pages.expand((p) => p.data ?? []).toList();
      return ListView.builder(
        itemCount: allPosts.length,
        itemBuilder: (_, i) => PostItem(allPosts[i]),
      );
    },
  ),
)

Mutations with MutationCubit #

Extend MutationCubit for server mutations:

class CreateUserMutationCubit extends MutationCubit<User, String> {
  @override
  Future<User> Function(String variables) get mutationFn => 
    (name) => api.createUser(name);

  @override
  MutationOptions<User, String>? get options => MutationOptions(
    onSuccess: (user) {
      QueryClient().invalidateQuery('users');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
}

class CreateUserScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CreateUserMutationCubit(),
      child: BlocBuilder<CreateUserMutationCubit, MutationState<User>>(
        builder: (context, state) {
          return Column(
            children: [
              if (state.isLoading)
                CircularProgressIndicator(),
              
              if (state.hasError)
                Text('Error: ${state.error}'),
              
              if (state.hasData)
                Text('Created: ${state.data!.name}'),
              
              ElevatedButton(
                onPressed: state.isLoading
                    ? null
                    : () {
                        context
                            .read<CreateUserMutationCubit>()
                            .mutate('John Doe');
                      },
                child: Text('Create User'),
              ),
            ],
          );
        },
      ),
    );
  }
}

Parallel Queries with MultiQueryBuilder #

Execute multiple queries in parallel using MultiQueryBuilder or NamedMultiQueryBuilder:

MultiQueryBuilder(
  configs: [
    MultiQueryConfig(
      queryKey: 'users'.toQueryKey(),
      queryFn: () => api.fetchUsers(),
    ),
    MultiQueryConfig(
      queryKey: 'posts'.toQueryKey(),
      queryFn: () => api.fetchPosts(),
    ),
    MultiQueryConfig(
      queryKey: 'comments'.toQueryKey(),
      queryFn: () => api.fetchComments(),
    ),
  ],
  builder: (context, state) {
    return Column(
      children: [
        if (!state.isAllSuccess) LinearProgressIndicator(),
        if (state.hasAnyError) ErrorBanner(),
        UsersList(state.getState<List<User>>(0)),
        PostsList(state.getState<List<Post>>(1)),
        CommentsList(state.getState<List<Comment>>(2)),
      ],
    );
  },
)

NamedMultiQueryBuilder(
  configs: [
    NamedQueryConfig(
      name: 'users',
      queryKey: 'users'.toQueryKey(),
      queryFn: () => api.fetchUsers(),
    ),
    NamedQueryConfig(
      name: 'posts',
      queryKey: 'posts'.toQueryKey(),
      queryFn: () => api.fetchPosts(),
    ),
    NamedQueryConfig(
      name: 'comments',
      queryKey: 'comments'.toQueryKey(),
      queryFn: () => api.fetchComments(),
    ),
  ],
  builder: (context, state) {
    return Column(
      children: [
        if (!state.isAllSuccess) LinearProgressIndicator(),
        if (state.hasAnyError) ErrorBanner(),
        UsersList(state.getState<List<User>>('users')),
        PostsList(state.getState<List<Post>>('posts')),
        CommentsList(state.getState<List<Comment>>('comments')),
      ],
    );
  },
)

Prefetching #

Warm the cache before data is needed:

PrefetchBuilder(
  configs: [
    PrefetchConfig(
      queryKey: 'users'.toQueryKey(),
      queryFn: () => api.fetchUsers(),
    ),
    PrefetchConfig(
      queryKey: 'posts'.toQueryKey(),
      queryFn: () => api.fetchPosts(),
    ),
  ],
  child: YourScreen(),
)

final prefetchCubit = PrefetchQueryCubit();
await prefetchCubit.prefetch('users', () => api.fetchUsers());

Dependent Queries #

Create dependent queries by implementing conditional logic:

class UserPostsQueryCubit extends QueryCubit<List<Post>> {
  final String userId;

  UserPostsQueryCubit(this.userId);

  @override
  String get key => 'posts:user:$userId';

  @override
  Future<List<Post>> Function() get queryFn => 
    () => api.fetchUserPosts(userId);

  @override
  QueryOptions? get options => QueryOptions(
    enabled: userId.isNotEmpty,
  );
}

Manual Refetch #

class UserList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () {
            context.read<UsersQueryCubit>().refetch();
          },
          child: Text('Refresh'),
        ),
      ],
    );
  }
}

Cache Invalidation #

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        final cubit = context.read<UsersQueryCubit>();
        
        cubit.invalidate();
        
        QueryClient().invalidateQuery('users');
        QueryClient().invalidateQueriesWithPrefix('user:');
      },
      child: Text('Invalidate Cache'),
    );
  }
}

API Reference #

QueryCubit #

abstract class QueryCubit<T> extends Cubit<QueryState<T>> {
  String get key;
  Future<T> Function() get queryFn;
  QueryOptions? get options => null;
  QueryClient? get client => null;
  
  void refetch();
  void invalidate();
}

Emits: 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

InfiniteQueryCubit #

abstract class InfiniteQueryCubit<TData, TParam> 
    extends Cubit<InfiniteQueryState<TData, TParam>> {
  String get key;
  Future<TData> Function(TParam param) get queryFn;
  InfiniteQueryOptions<TData, TParam>? get options => null;
  
  Future<void> fetchNextPage([TParam? param]);
  Future<void> fetchPreviousPage();
  Future<void> refetchPage(int index);
  void reset();
}

MutationCubit #

abstract class MutationCubit<TData, TVariables> 
    extends Cubit<MutationState<TData>> {
  Future<TData> Function(TVariables variables) get mutationFn;
  MutationOptions<TData, TVariables>? get options => null;
  QueryClient? get client => null;
  
  Future<void> mutate(TVariables variables);
  void reset();
}

Emits: MutationState<TData> with:

  • isLoading - Whether mutation is in progress
  • data - Mutation result
  • error - Mutation error
  • hasData - Whether mutation succeeded
  • hasError - Whether mutation failed

Why Bloc? #

If you're already using flutter_bloc, this adapter provides seamless integration with Flutter Query:

  • Structured - Bloc's explicit state management
  • Testable - Easy to test cubits
  • Familiar - Use BlocBuilder/BlocConsumer as usual
  • Debuggable - Bloc DevTools integration

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!);
  },
)

Bloc Adapter (QueryCubit):

class UsersQueryCubit extends QueryCubit<List<User>> {
  @override
  String get key => 'users';
  
  @override
  Future<List<User>> Function() get queryFn => () => api.fetchUsers();
}

BlocProvider(
  create: (_) => UsersQueryCubit(),
  child: BlocBuilder<UsersQueryCubit, QueryState<List<User>>>(
    builder: (context, state) {
      if (state.isLoading) return Loading();
      return UserList(state.data!);
    },
  ),
)

Both approaches use the same underlying query engine and have identical performance.

Advanced Usage #

Using BlocConsumer for Side Effects #

BlocConsumer<UsersQueryCubit, QueryState<List<User>>>(
  listener: (context, state) {
    if (state.hasError) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error: ${state.error}')),
      );
    }
    
    if (state.isFetching) {
      print('Background refresh in progress...');
    }
  },
  builder: (context, state) {
    return UserList(users: state.data ?? []);
  },
)

Multiple Queries in One Screen #

MultiBlocProvider(
  providers: [
    BlocProvider(create: (_) => UsersQueryCubit()),
    BlocProvider(create: (_) => PostsQueryCubit()),
  ],
  child: MyScreen(),
)

Custom QueryClient #

class SecureUsersQueryCubit extends QueryCubit<String> {
  @override
  String get key => 'auth-token';
  
  @override
  Future<String> Function() get queryFn => () => api.getAuthToken();
  
  @override
  QueryOptions? get options => QueryOptions(
    isSecure: true,
    maxAge: Duration(minutes: 15),
    staleTime: Duration(minutes: 5),
  );
  
  @override
  QueryClient? get client => QueryClient();
}

Security Features πŸ”’ #

fasq_bloc supports all FASQ security features through QueryClient configuration and options:

Secure Queries #

class SecureTokenQueryCubit extends QueryCubit<String> {
  @override
  String get key => 'auth-token';
  
  @override
  Future<String> Function() get queryFn => () => api.getAuthToken();
  
  @override
  QueryOptions? get options => QueryOptions(
    isSecure: true,
    maxAge: Duration(minutes: 15),
    staleTime: Duration(minutes: 5),
  );
}

Secure Mutations #

class SecureMutationCubit extends MutationCubit<String, String> {
  @override
  Future<String> Function(String variables) get mutationFn => 
    (data) => api.secureMutation(data);
  
  @override
  MutationOptions<String, String>? get options => MutationOptions(
    queueWhenOffline: true,
    maxRetries: 3,
  );
}

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
58
downloads

Publisher

verified publishershafi.dev

Weekly Downloads

Bloc/Cubit adapter for FASQ (Flutter Async State Query) - async state management with Bloc

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

bloc, fasq, flutter, flutter_bloc

More

Packages that depend on fasq_bloc