Reputation: 261
When you insert/remove/reorder (or do any other manipulation) ListView
items according to the default behaviour of ListView.builder
and ListView.separated
it always rebuilds the whole widget.
How can I avoid this? It brings undesired results such as loss of data.
Upvotes: 5
Views: 7750
Reputation: 531
Just add cacheExtent as 9999 in the Listview, below is explanation of CacheExtent
The viewport has an area before and after the visible area to cache items that are about to become visible when the user scrolls. Items that fall in this cache area are laid out even though they are not (yet) visible on screen. The cacheExtent describes how many pixels the cache area extends before the leading edge and after the trailing edge of the viewport.
Example
ListView.builder(
cacheExtent: 9999,
padding: EdgeInsets.only(bottom: 100, top: 8),
itemCount: item.length,
itemBuilder: (context, index) {
});
Upvotes: 3
Reputation: 313
TL:DR: I recommend switching from ListView to a Column and wrapping the Column into a SingleChildScrollView.
I’ve been using Flutter Hooks to build animations into stateless widgets.
I’ve ran into the same problem as OP, widgets are rebuilt when entering screen and the animations run again every time they are built.
If you have a very long list of items, let me say I don’t have a great solution for you, given the extensive optimizations built into ListView.
But if you have a small list, I recommend switching from ListView to a Column and wrapping the Column into a SingleChildScrollView. For me it fixed the problem.
Upvotes: 4
Reputation: 1470
There are a bunch of misconceptions related to ListView.
First, ListView.builder does not "always rebuilds the whole widget" in the sense that it rebuilds all children. Instead, it only rebuilds the necessary children, i.e. the ones visible. If your ListView is rebuilding all children, check your code, there is something wrong with it.
Second, official docs actually recommend using ListView.builder
or ListView.separated
when working with long lists. (Docs)
Third, there is NO WAY to completely control what is built by the ListView using any constructor. I would love for somebody to prove me wrong on this point. If that was the case, there would be a builder with a callback on which children to rebuild. There isn't. And that is not what findChildIndexCallback
does.
Fourth, ListView.custom
with findChildIndexCallback
is useful to preserve the state of the child.
From the docs: findChildIndexCallback property (Link)
If not provided, a child widget may not map to its existing RenderObject when the order of children returned from the children builder changes. This may result in state-loss.
That is, if you NEED to CHANGE the state of the widget AND MAINTAIN, this is useful. Again, change and keep. In most cases this is not needed.
In summary, building and rebuilding is not expensive, as long as your data is loaded upfront using init. If you need to manipulate the source data (like loading images) you can do it using any constructor.
To better understand the builder, try the code below and scroll up. You will understand that the builder is called for the items 'randomly' from around 11 to 14. Full code:
import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(context) => MaterialApp(
home: const MyListView(),
);
}
class MyListView extends StatefulWidget {
const MyListView({Key? key}) : super(key: key);
@override
State<MyListView> createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
final items = List<String>.generate(10000, (i) => 'Item $i');
void _addItems() {
setState(() {
items.add("asdf");
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
print(index);
return ListTile(
title: Text(items[index]),
);
},
),
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
onPressed: () => _addItems(),
child: const Text('Add items'),
),
],
),
),
);
}
}
Upvotes: 3
Reputation: 261
Instead of using ListView.builder
or ListView.separated
you can use ListView.custom
by setting findChildIndexCallback
property
ListView.custom(
key: Key('messageListView'),
controller: _scrollController,
reverse: true,
childrenDelegate: SliverChildBuilderDelegate(
(context, i) {
return Container(key: ValueKey('message-${message.id}'));
},
childCount: _messages.length,
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key;
return _messages
.indexWhere((m) => 'message-${m.id}' == valueKey.value);
},
),
);
Upvotes: 11