Narek
Narek

Reputation: 261

Flutter - avoid ListView rebuild

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

Answers (4)

Rohan Arora
Rohan Arora

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

Ionut Vasile
Ionut Vasile

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

Roddy R
Roddy R

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

Narek
Narek

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

Related Questions