Introduction
Lazy loading and infinite scrolling are essential techniques in mobile development to improve app performance and user experience. In Flutter, you can defer loading large datasets by fetching chunks of data as needed and appending them to a scrolling list. This tutorial covers how to implement lazy loading with ListView.builder, detect when the user reaches the list’s end, optimize rendering with caching and placeholders, and handle errors and retries gracefully.
Setup Your Flutter Project
Start by creating a new Flutter project or open an existing one. Add the http package to fetch data from an API. In your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
http
Run flutter pub get. Then create a service class to fetch paginated data. For simplicity, simulate network calls with Future.delayed.
class ApiService {
Future<List<String>> fetchItems(int page, int limit) async {
await Future.delayed(Duration(seconds: 2));
return List.generate(limit, (i) => 'Item ${page * limit + i + 1}');
}
}Building a Lazy Loading ListView
Use ListView.builder to render only visible items. Track a list of fetched items and a loading flag. When the builder’s index reaches the item count, show a loading indicator and trigger the next fetch.
class LazyList extends StatefulWidget {
@override
_LazyListState createState() => _LazyListState();
}
class _LazyListState extends State<LazyList> {
final ApiService api = ApiService();
List<String> items = [];
bool isLoading = false;
int page = 0;
final int limit = 20;
@override
void initState() {
super.initState();
_loadMore();
}
void _loadMore() async {
if (isLoading) return;
setState(() => isLoading = true);
final newItems = await api.fetchItems(page, limit);
setState(() {
page++;
items.addAll(newItems);
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length + (isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= items.length) return Center(child: CircularProgressIndicator());
return ListTile(title: Text(items[index]));
},
);
}
}
This pattern ensures the list grows dynamically and shows a spinner while fetching the next batch.
Implementing Infinite Scrolling with ScrollController
To automatically fetch more items when the user scrolls close to the bottom, attach a ScrollController and listen for position changes.
@override
void initState() {
super.initState();
_controller = ScrollController()
..addListener(() {
if (_controller.position.pixels >=
_controller.position.maxScrollExtent - 200) {
_loadMore();
}
});
_loadMore();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _controller,
itemCount: items.length + (isLoading ? 1 : 0),
itemBuilder: ...
);
}
Adjust the threshold (200px above bottom) to prefetch data. Always check isLoading to prevent duplicate calls.
Optimizing Performance with Caching and Placeholders
Rendering images or large widgets in a lazy list can cause jank. Use the cached_network_image package for images or preallocate placeholder widgets. Example usage:
CachedNetworkImage(
imageUrl: url,
placeholder: (ctx, _) => Container(color: Colors.grey[200]),
errorWidget: (ctx, _, __) => Icon(Icons.error),
);
In addition, wrap list items in const constructors and avoid heavy builds in the itemBuilder. If data is static, consider using the AutomaticKeepAliveClientMixin to preserve item state.
Error Handling and Retry Mechanisms
Network errors are inevitable. Wrap API calls in try-catch and expose an error state. Show a retry button at the bottom when loading fails.
void _loadMore() async {
if (isLoading || hasError) return;
setState(() {
isLoading = true;
hasError = false;
});
try {
final newItems = await api.fetchItems(page, limit);
setState(() {
page++;
items.addAll(newItems);
});
} catch (e) {
setState(() => hasError = true);
} finally {
setState(() => isLoading = false);
}
}
if (hasError) return TextButton(onPressed: _loadMore, child: Text('Retry'));
This ensures users can recover from failures and continue scrolling seamlessly.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
Lazy loading lists and infinite scrolling in Flutter apps offer a smooth user experience while keeping resource usage low. Leveraging ListView.builder, ScrollController, and performance optimizations like caching and placeholders delivers responsive interfaces. Finally, robust error handling with retry options ensures resilience. Apply these patterns to any paginated API or large dataset to enhance your mobile development workflow.