Flutter ListView Optimization with Riverpod: Avoiding Unnecessary Rebuilds
Efficient state management is key to building scalable, high-performance Flutter applications — especially when working with dynamic UI elements like ListView.builder. A common performance bottleneck is unnecessary widget rebuilds when list data changes. In this article, we’ll dive into a fully optimized Flutter example using Riverpod that avoids this issue through smart architecture.
🧱 The Problem: Unnecessary Rebuilds
Imagine a list of buttons, each displaying a number. When a button is pressed, its value increases — simple, right? But in many naive implementations, updating one item causes every widget in the list to rebuild, leading to performance issues, especially with large lists.
🎯 The Goal
We want:
- ✅ A scrollable list of buttons.
- ✅ Pressing one button should only rebuild that item, not the whole list.
- ✅ Use Riverpod for clean and predictable state management.
- ✅ Maintain immutability and performance best practices.
✅ The Optimized Solution
Here’s the code that solves the problem efficiently:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(ProviderScope(child: MaterialApp(home: HomeScreen())));
}
final elements = StateProvider<List<int>>(
(ref) => List.generate(50, (index) {
return index;
}),
);
final _index = StateProvider<int>((ref) => 0);
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
print("Rebuilds.......");
return Scaffold(
appBar: AppBar(title: Text("App optimization")),
body: Consumer(
builder: (context, ref, child) => ListView.builder(
itemCount: ref.watch(elements.select((item) => item.length)),
itemBuilder: (context, index) {
print("Listview Items rebuild $index");
return Padding(
padding: const EdgeInsets.all(8.0),
child: ProviderScope(
overrides: [_index.overrideWith((ref) => index)],
child: buildButton(),
),
);
},
),
),
);
}
}
buildButton() {
return Consumer(
builder: (context, ref, child) {
final indexOfItem = ref.read(_index);
return ElevatedButton(
onPressed: () {
print("Length of List ${ref.read(elements).length}");
final list = [...ref.read(elements)];
int val = indexOfItem + 10;
list[indexOfItem] = val;
ref.read(elements.notifier).state = list; // Assign new list to state
},
child: Text("data ${ref.watch(elements)[indexOfItem]}"),
);
},
);
}🚀 How It Works
1. Minimized Rebuilds with `.select`
ref.watch(elements.select((list) => list.length))Only watches the length of the list, not the content. The ListView.builder won’t rebuild unless items are added or removed.
2. Scoped Providers Per List Item
ProviderScope(overrides: [_index.overrideWith((ref) => index)])This technique allows each button to access its own index via a local override, effectively creating isolated rebuild zones.
3. Immutable State Updates
final list = [...ref.read(elements)];By copying the list before modifying, we avoid mutating the state directly. This adheres to best practices and ensures proper state detection in Riverpod.
4. Only the Affected Button Rebuilds
Each button uses its scoped _index, and the Consumer around it listens only to the item at that index. This means:
- Pressing “data 2” only rebuilds that specific button.
- “data 0”, “data 1”, “data 3”, etc., remain untouched.
📈 Performance Benefits
| Technique | Benefit |
Get Saqlain Ali Shah’s stories in your inbox
Join Medium for free to get updates from this writer.
| — — — — — — — — — — — — — | — — — — — — — — — — — — — — — — — — — — — — |
| .select() | Prevents full-list rebuilds on small changes |
| Scoped provider overrides | Isolates rebuilds to the changed item |
| Immutable list updates | Ensures safe, predictable state transitions |
| StatelessWidget + Consumer | Avoids unnecessary stateful widget overhead |
🧪 Debug Output (Optional)
Use print() statements to verify:
"Rebuilds......."is only printed once when the screen builds."ListView item builder called for index X"is printed once per item.- Tapping a button prints only that index’s rebuild info.
🧼 Final Thoughts
This example showcases a clean and scalable approach to managing dynamic lists in Flutter using Riverpod. With scoped state and precise rebuild control, your UI becomes faster, more efficient, and easier to maintain — especially in large applications.










