I have a ListView.builder using a Consumer of a simple Provider. Each of the items needs to load something asynchronously so I'm using FutureBuilder.
When I add an item to the list, the entire list is rebuilt. This makes sense because it's wrapped in the Consumer, but unfortunately it means the existing items need to re-run the future despite those items not actually changing.
class ListWithButton extends StatelessWidget {
const ListWithButton({super.key});
@override
Widget build(BuildContext context) {
return Consumer<MyProvider>(
builder: (_, provider, __) => Column(
children: [
ElevatedButton(
onPressed: () {
provider.addItem();
},
child: const Text("Add Item"),
),
Expanded(
child: ListView.builder(
itemCount: provider.items.length,
itemBuilder: (_, index) => FutureBuilder<Color>(
future: provider.loadSomething(index),
builder: (_, snapshot) => ListTile(
leading: Container(
width: 30,
height: 30,
color: snapshot.connectionState == ConnectionState.done
? snapshot.data!
: Colors.red,
),
title: Text(index.toString()),
),
),
),
),
],
),
);
}
}
class MyProvider extends ChangeNotifier {
final List<int> _items = [];
List<int> get items => _items;
void addItem() {
_items.add(_items.length);
notifyListeners();
}
Future<Color> loadSomething(int i) async {
await Future.delayed(Duration(seconds: 1));
return Colors.green;
}
}
Is there a way I can only rebuild the items in the ListView which need building (i.e. the new items)?
You cannot necessarily prevent the list from rebuilding its child widgets when a new child is added, but you can prevent the Futures from being re-initialized by initializing and storing them outside of the build method, which is recommended in the documentation for FutureBuilder:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
To do this, you need only change MyProvider, like so:
class MyProvider extends ChangeNotifier {
final List<int> _items = [];
final Map<int, Future<Color>> _futureColors = {};
List<int> get items => _items;
void addItem() {
_items.add(_items.length);
notifyListeners();
}
Future<Color>? loadSomething(int i) {
if (!_futureColors.containsKey(i)) {
_futureColors[i] = Future.delayed(
const Duration(seconds: 1),
() => Colors.green,
);
}
return _futureColors[i];
}
}
Here we only create a new future the first time loadSomething
is called with a given index — We store that future in a Map, and on later calls to loadSomething
we return the Future that has already been initialized for that index. (The reason I used a Map
instead of a List
is that there have no guarantees that the function will receive valid List indices).
Note also that loadSomething
is no longer an async
function — This is important, as we want the future to be returned to the FutureBuilder
synchronously. If we return the future asynchronously, you may notice that the elements of the List still flicker red sometimes when you add new elements.