fasq_bloc 0.2.0 copy "fasq_bloc: ^0.2.0" to clipboard
fasq_bloc: ^0.2.0 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 - Cubit wrapper for queries
  • ♾️ InfiniteQueryCubit - Infinite queries for pagination
  • πŸ”„ MutationCubit - 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 #

Infinite Queries with InfiniteQueryCubit #

BlocProvider(
  create: (_) => InfiniteQueryCubit<List<Post>, int>(
    key: 'posts',
    queryFn: (page) => api.fetchPosts(page: page),
    options: InfiniteQueryOptions(
      getNextPageParam: (pages, last) => pages.length + 1,
    ),
  ),
  child: BlocBuilder<InfiniteQueryCubit<List<Post>, int>, InfiniteQueryState<List<Post>, int>>(
    builder: (context, state) {
      return ListView.builder(
        itemCount: state.pages.expand((p) => p.data ?? []).length,
        itemBuilder: (_, i) => Text('Item #$i'),
      );
    },
  ),
)

Parallel Queries with MultiQueryBuilder #

Execute multiple queries in parallel using MultiQueryBuilder or NamedMultiQueryBuilder:

// Index-based access
MultiQueryBuilder(
  configs: [
    MultiQueryConfig(key: 'users', queryFn: () => api.fetchUsers()),
    MultiQueryConfig(key: 'posts', queryFn: () => api.fetchPosts()),
    MultiQueryConfig(key: 'comments', 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)),
      ],
    );
  },
)

// Named access (better DX)
NamedMultiQueryBuilder(
  configs: [
    NamedQueryConfig(name: 'users', key: 'users', queryFn: () => api.fetchUsers()),
    NamedQueryConfig(name: 'posts', key: 'posts', queryFn: () => api.fetchPosts()),
    NamedQueryConfig(name: 'comments', key: 'comments', 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:

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

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

Dependent Queries #

final userCubit = QueryCubit<User>(key: 'user', queryFn: fetchUser);
final postsCubit = QueryCubit<List<Post>>(
  key: 'posts:user:${userCubit.state.data?.id}',
  queryFn: () => fetchPosts(userCubit.state.data!.id),
  options: const QueryOptions(enabled: false),
);

// Enable when user loaded
if (userCubit.state.isSuccess) {
  // You can recreate with enabled true or structure initialization post user
}

Basic Query with QueryCubit #

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

class UsersScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => QueryCubit<List<User>>(
        key: 'users',
        queryFn: () => api.fetchUsers(),
        options: QueryOptions(
          staleTime: Duration(minutes: 5),
        ),
      ),
      child: BlocBuilder<QueryCubit<List<User>>, 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();
        },
      ),
    );
  }
}

Manual Refetch #

class UserList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () {
            // Refetch the query
            context.read<QueryCubit<List<User>>>().refetch();
          },
          child: Text('Refresh'),
        ),
        // ... list content
      ],
    );
  }
}

Mutations with MutationCubit #

class CreateUserScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => MutationCubit<User, String>(
        mutationFn: (name) => api.createUser(name),
        onSuccessCallback: (user) {
          print('Created user: ${user.name}');
          // Invalidate users query
          QueryClient().invalidateQuery('users');
        },
        onErrorCallback: (error) {
          print('Error: $error');
        },
      ),
      child: BlocBuilder<MutationCubit<User, String>, 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<MutationCubit<User, String>>()
                            .mutate('John Doe');
                      },
                child: Text('Create User'),
              ),
            ],
          );
        },
      ),
    );
  }
}

Cache Invalidation #

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        final cubit = context.read<QueryCubit<List<User>>>();
        
        // Invalidate and refetch this query
        cubit.invalidate();
        
        // Or use QueryClient directly
        QueryClient().invalidateQuery('users');
        QueryClient().invalidateQueriesWithPrefix('user:');
      },
      child: Text('Invalidate Cache'),
    );
  }
}

API Reference #

QueryCubit #

class QueryCubit<T> extends Cubit<QueryState<T>> {
  QueryCubit({
    required String key,
    required Future<T> Function() queryFn,
    QueryOptions? options,
  });
  
  void refetch(); // Manually refetch
  void invalidate(); // Invalidate and refetch
}

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

MutationCubit #

class MutationCubit<TData, TVariables> extends Cubit<MutationState<TData>> {
  MutationCubit({
    required Future<TData> Function(TVariables) mutationFn,
    void Function(TData)? onSuccessCallback,
    void Function(Object)? onErrorCallback,
  });
  
  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):

BlocProvider(
  create: (_) => QueryCubit(
    key: 'users',
    queryFn: () => api.fetchUsers(),
  ),
  child: BlocBuilder<QueryCubit<List<User>>, 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<QueryCubit<User>, QueryState<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) {
    // Build UI
  },
)

Multiple Queries in One Screen #

MultiBlocProvider(
  providers: [
    BlocProvider(
      create: (_) => QueryCubit<List<User>>(
        key: 'users',
        queryFn: () => api.fetchUsers(),
      ),
    ),
    BlocProvider(
      create: (_) => QueryCubit<List<Post>>(
        key: 'posts',
        queryFn: () => api.fetchPosts(),
      ),
    ),
  ],
  child: MyScreen(),
)

Security Features πŸ”’ #

fasq_bloc supports all FASQ security features through QueryClient configuration:

Secure Queries with QueryCubit #

BlocProvider(
  create: (_) => QueryCubit<String>(
    key: 'auth-token',
    queryFn: () => 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
  ),
  child: BlocBuilder<QueryCubit<String>, QueryState<String>>(
    builder: (context, state) {
      // Secure data never persisted, cleared on app background
      return Text('Token: ${state.data}');
    },
  ),
)

Secure Mutations with MutationCubit #

BlocProvider(
  create: (_) => MutationCubit<String, String>(
    mutationFn: (data) => api.secureMutation(data),
    options: MutationOptions(
      queueWhenOffline: true,
      maxRetries: 3,
    ),
    client: context.queryClient,         // Use configured client
  ),
  child: BlocBuilder<MutationCubit<String, String>, MutationState<String>>(
    builder: (context, state) {
      return ElevatedButton(
        onPressed: state.isLoading
            ? null
            : () => context.read<MutationCubit<String, String>>().mutate('secure-data'),
        child: state.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
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