Skip to content

Prefetching with Bloc

The Bloc adapter provides widgets and cubits for prefetching queries in your Bloc-based Flutter applications.

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

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'),
],
),
),
);
}
}
  • prefetch<T>: Prefetch a single query
  • prefetchAll: Prefetch multiple queries in parallel

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

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

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

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,
);
}
}
// Usage
GoRoute(
path: '/dashboard',
builder: (context, state) => PrefetchRoute(
configs: [
PrefetchConfig(queryKey: 'dashboard-data'.toQueryKey(), queryFn: () => api.fetchDashboardData()),
],
child: DashboardPage(),
),
),

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 prefetching
PrefetchBuilder(
configs: [PrefetchConfig(queryKey: 'data'.toQueryKey(), queryFn: fetchData)],
child: MyWidget(),
);
// More complex: Manual cubit management
class 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();
}

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());

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

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.