Flutter - avoid ListView rebuild
Asked Answered
P

4

5

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.

Phosphaturia answered 25/12, 2020 at 13:20 Comment(0)
P
11

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);
          },
        ),
      );
Phosphaturia answered 25/12, 2020 at 13:20 Comment(2)
Hi, I would like to try this approach - so far looks like the only option. However I was not able to get this to work correctly. I saw a similar answer in github as well github.com/felangel/bloc/issues/1965#issuecomment-927945015 . If you have some time, I'd like to kindly request if you could please update your answer with a simple hello-world main.dart. Here's a post with example of a minimal main.dart implementation poster uses to demo the solution there https://mcmap.net/q/1921171/-flutter-chat-screen-built-with-a-streambuilder-showing-messages-multiple-times , Thank youSequel
This doesn't prevent any rebuilds. I don't understand why upvotes. See github.com/flutter/flutter/issues/99778Refinery
U
4

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.

Unsnap answered 12/12, 2022 at 23:27 Comment(0)
R
2

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'),
            ),
          ],
        ),
      ),
    );
  }
}
Refinery answered 13/6, 2022 at 23:24 Comment(1)
What about the AutomaticKeepAliveClientMixin? It gives you some control.Toluca
R
2

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) {
    });
Rebecarebecca answered 19/4 at 10:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.