Prefetching with Bloc
The Bloc adapter provides widgets and cubits for prefetching queries in your Bloc-based Flutter applications.
PrefetchBuilder Widget
Section titled “PrefetchBuilder Widget”The PrefetchBuilder widget prefetches queries when it mounts and renders its child:
class Dashboard extends StatelessWidget { @override Widget build(BuildContext context) { return PrefetchBuilder( configs: [ PrefetchConfig(queryKey: 'user-stats'.toQueryKey(), queryFn: () => api.fetchUserStats()), PrefetchConfig(queryKey: 'recent-posts'.toQueryKey(), queryFn: () => api.fetchRecentPosts()), PrefetchConfig(queryKey: 'notifications'.toQueryKey(), queryFn: () => api.fetchNotifications()), ], child: DashboardContent(), ); }}Features
Section titled “Features”- Mount Trigger: Prefetches when the widget mounts
- Parallel Execution: All queries are prefetched in parallel
- Cleanup: Automatically disposes the cubit when unmounted
- Transparent: Child widget is rendered immediately
PrefetchQueryCubit
Section titled “PrefetchQueryCubit”The PrefetchQueryCubit provides direct control over prefetching:
class UserCard extends StatefulWidget { final String userId;
const UserCard({required this.userId});
@override State<UserCard> createState() => _UserCardState();}
class _UserCardState extends State<UserCard> { late final PrefetchQueryCubit _prefetchCubit;
@override void initState() { super.initState(); _prefetchCubit = PrefetchQueryCubit(); }
@override void dispose() { _prefetchCubit.close(); super.dispose(); }
@override Widget build(BuildContext context) { return Card( child: InkWell( onTap: () => Navigator.pushNamed(context, '/user/${widget.userId}'), onHover: () => _prefetchCubit.prefetch( 'user-${widget.userId}'.toQueryKey(), () => api.fetchUser(widget.userId), ), child: Column( children: [ Text('User ${widget.userId}'), Text('Hover to prefetch profile data'), ], ), ), ); }}Methods
Section titled “Methods”prefetch<T>: Prefetch a single queryprefetchAll: Prefetch multiple queries in parallel
Advanced Patterns
Section titled “Advanced Patterns”Route-Based Prefetching
Section titled “Route-Based Prefetching”Prefetch data before navigation:
class UserList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( itemBuilder: (context, index) { final userId = users[index].id;
return ListTile( title: Text(users[index].name), onTap: () { // Prefetch user details before navigation final prefetchCubit = PrefetchQueryCubit(); prefetchCubit.prefetch('user-$userId'.toQueryKey(), () => api.fetchUser(userId)); Navigator.pushNamed(context, '/user/$userId'); }, ); }, ); }}Tab-Based Prefetching
Section titled “Tab-Based Prefetching”Prefetch data for inactive tabs:
class TabbedInterface extends StatefulWidget { @override State<TabbedInterface> createState() => _TabbedInterfaceState();}
class _TabbedInterfaceState extends State<TabbedInterface> { late final PrefetchQueryCubit _prefetchCubit;
@override void initState() { super.initState(); _prefetchCubit = PrefetchQueryCubit(); }
@override void dispose() { _prefetchCubit.close(); super.dispose(); }
@override Widget build(BuildContext context) { return DefaultTabController( length: 3, child: Column( children: [ TabBar( onTap: (index) { // Prefetch data for other tabs switch (index) { case 0: _prefetchCubit.prefetchAll([ PrefetchConfig(queryKey: 'posts'.toQueryKey(), queryFn: () => api.fetchPosts()), PrefetchConfig(queryKey: 'comments'.toQueryKey(), queryFn: () => api.fetchComments()), ]); break; case 1: _prefetchCubit.prefetchAll([ PrefetchConfig(queryKey: 'users'.toQueryKey(), queryFn: () => api.fetchUsers()), PrefetchConfig(queryKey: 'comments'.toQueryKey(), queryFn: () => api.fetchComments()), ]); break; case 2: _prefetchCubit.prefetchAll([ PrefetchConfig(queryKey: 'users'.toQueryKey(), queryFn: () => api.fetchUsers()), PrefetchConfig(queryKey: 'posts'.toQueryKey(), queryFn: () => api.fetchPosts()), ]); break; } }, tabs: [ Tab(text: 'Users'), Tab(text: 'Posts'), Tab(text: 'Comments'), ], ), Expanded( child: TabBarView( children: [ UsersTab(), PostsTab(), CommentsTab(), ], ), ), ], ), ); }}Conditional Prefetching
Section titled “Conditional Prefetching”Prefetch based on conditions:
class UserProfile extends StatefulWidget { final String userId; final bool shouldPrefetchRelated;
@override State<UserProfile> createState() => _UserProfileState();}
class _UserProfileState extends State<UserProfile> { late final PrefetchQueryCubit _prefetchCubit;
@override void initState() { super.initState(); _prefetchCubit = PrefetchQueryCubit();
if (widget.shouldPrefetchRelated) { _prefetchCubit.prefetchAll([ PrefetchConfig(queryKey: 'user-posts-${widget.userId}'.toQueryKey(), queryFn: () => api.fetchUserPosts(widget.userId)), PrefetchConfig(queryKey: 'user-followers-${widget.userId}'.toQueryKey(), queryFn: () => api.fetchUserFollowers(widget.userId)), ]); } }
@override void dispose() { _prefetchCubit.close(); super.dispose(); }
@override Widget build(BuildContext context) { return ProfileContent(); }}Integration with Routing
Section titled “Integration with Routing”Go Router Integration
Section titled “Go Router Integration”class AppRouter { static final router = GoRouter( routes: [ GoRoute( path: '/users', builder: (context, state) { return PrefetchBuilder( configs: [ PrefetchConfig(queryKey: 'users'.toQueryKey(), queryFn: () => api.fetchUsers()), ], child: UsersPage(), ); }, routes: [ GoRoute( path: '/:userId', builder: (context, state) { final userId = state.pathParameters['userId']!; return PrefetchBuilder( configs: [ PrefetchConfig(queryKey: 'user-$userId'.toQueryKey(), queryFn: () => api.fetchUser(userId)), PrefetchConfig(queryKey: 'user-posts-$userId'.toQueryKey(), queryFn: () => api.fetchUserPosts(userId)), ], child: UserProfilePage(userId: userId), ); }, ), ], ), ], );}Custom Route Wrapper
Section titled “Custom Route Wrapper”Create a reusable route wrapper for prefetching:
class PrefetchRoute extends StatelessWidget { final List<PrefetchConfig> configs; final Widget child;
const PrefetchRoute({ required this.configs, required this.child, });
@override Widget build(BuildContext context) { return PrefetchBuilder( configs: configs, child: child, ); }}
// UsageGoRoute( path: '/dashboard', builder: (context, state) => PrefetchRoute( configs: [ PrefetchConfig(queryKey: 'dashboard-data'.toQueryKey(), queryFn: () => api.fetchDashboardData()), ], child: DashboardPage(), ),),Performance Tips
Section titled “Performance Tips”1. Use PrefetchBuilder for Mount-Based Prefetching
Section titled “1. Use PrefetchBuilder for Mount-Based Prefetching”For prefetching when components mount, use PrefetchBuilder:
// Good: Simple mount-based prefetchingPrefetchBuilder( configs: [PrefetchConfig(queryKey: 'data'.toQueryKey(), queryFn: fetchData)], child: MyWidget(),);
// More complex: Manual cubit managementclass MyWidget extends StatefulWidget { @override State<MyWidget> createState() => _MyWidgetState();}
class _MyWidgetState extends State<MyWidget> { late final PrefetchQueryCubit _cubit;
@override void initState() { super.initState(); _cubit = PrefetchQueryCubit(); _cubit.prefetch('data'.toQueryKey(), fetchData); }
@override void dispose() { _cubit.close(); super.dispose(); }
@override Widget build(BuildContext context) => MyWidgetContent();}2. Batch Related Prefetches
Section titled “2. Batch Related Prefetches”Use prefetchAll for multiple related queries:
// Good: Batch prefetch_prefetchCubit.prefetchAll([ PrefetchConfig(queryKey: 'user'.toQueryKey(), queryFn: () => api.fetchUser()), PrefetchConfig(queryKey: 'posts'.toQueryKey(), queryFn: () => api.fetchPosts()),]);
// Less efficient: Individual prefetches_prefetchCubit.prefetch('user'.toQueryKey(), () => api.fetchUser());_prefetchCubit.prefetch('posts'.toQueryKey(), () => api.fetchPosts());3. Reuse Cubit Instances
Section titled “3. Reuse Cubit Instances”For components that need frequent prefetching, reuse the cubit:
class UserCard extends StatefulWidget { @override State<UserCard> createState() => _UserCardState();}
class _UserCardState extends State<UserCard> { late final PrefetchQueryCubit _prefetchCubit;
@override void initState() { super.initState(); _prefetchCubit = PrefetchQueryCubit(); }
@override void dispose() { _prefetchCubit.close(); super.dispose(); }
@override Widget build(BuildContext context) { return Card( child: InkWell( onHover: () => _prefetchCubit.prefetch('user-data'.toQueryKey(), fetchUserData), onTap: () => _prefetchCubit.prefetch('user-details'.toQueryKey(), fetchUserDetails), child: UserCardContent(), ), ); }}Testing
Section titled “Testing”Test prefetching widgets and cubits:
testWidgets('PrefetchBuilder prefetches on mount', (tester) async { int fetchCount = 0;
Future<String> fetchData() async { fetchCount++; return 'test-data'; }
await tester.pumpWidget( MaterialApp( home: PrefetchBuilder( configs: [ PrefetchConfig(queryKey: 'test-key'.toQueryKey(), queryFn: fetchData), ], child: SizedBox(), ), ), );
await tester.pump(); expect(fetchCount, equals(1));});
test('PrefetchQueryCubit prefetches correctly', () async { final cubit = PrefetchQueryCubit(); int fetchCount = 0;
Future<String> fetchData() async { fetchCount++; return 'test-data'; }
await cubit.prefetch('test-key'.toQueryKey(), fetchData);
expect(fetchCount, equals(1));
cubit.close();});The Bloc adapter provides a clean, widget-based approach to prefetching that integrates seamlessly with Flutter’s widget lifecycle and Bloc patterns.