1

I'm investigating performance difference between ListView and ListView.builder in Flutter. Theory says ListView create all widget at once, while ListView.builder create when it's visible.

I checked with Devtool that it's true. ListView create all widgets and keep them in memory, but when I scroll ListView, Widget are disposed and created when they come visible/invisible, I mean when they are or not in screen.

How it can be possible? If all widgets in ListView are already in memory, why are created and disposed?

You can check this behaviour with this code:

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: PageOne());
  }
}

class PageOne extends StatelessWidget {
  const PageOne({super.key});

  @override
  Widget build(BuildContext context) {
    final items = List.generate(500000, (index) => Item(value: index));

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ThirdPage(items: items),
                  )),
              child: const Text('ListView'),
            ),
          ],
        ),
      ),
    );
  }
}

class ThirdPage extends StatelessWidget {
  const ThirdPage({super.key, required this.items});

  final List<Item> items;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: items.map((e) => _Item(item: e)).toList(),
      ),
    );
  }
}

class _Item extends StatefulWidget {
  const _Item({required this.item});

  final Item item;

  @override
  State<_Item> createState() => _ItemState();
}

class _ItemState extends State<_Item> {
  @override
  void initState() {
    super.initState();
    print('Create widget ${widget.item.value}');
  }

  @override
  void dispose() {
    print('Delete widget ${widget.item.value}');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text('${widget.item.value}');
  }
}
5
  • when you use ListView all the children are created at once and are never disposed, with ListView.builder they are created / disposed only when are visible / hidden Commented Aug 19, 2024 at 19:49
  • That not true. You can checked with the code above. Commented Aug 19, 2024 at 19:54
  • @Manuel The only situation i can agree with you on, is 'when that listview is created in a very very long screen ' while the user scrolls up and down , suppose it's placed in the bottom of that screen . so when you are at the top of that screen . it may be disposed and recreated when going down. Commented Aug 19, 2024 at 20:33
  • you are confusing widgets with states - when you use ListView you have to pass a list with already created widgets, with ListView.builder they are created on demand Commented Aug 19, 2024 at 20:59
  • @Manuel very instructive. Btw, your example code would be clearer if you amend the messages: "Create widget" -> "Init state", "Delete widget" -> "Dispose state". You could also print a message in build: "Building widget: ...". And you could add the class Item. Commented Aug 20, 2024 at 5:36

1 Answer 1

1

What you are observing is that when you are viewing the list of items and scrolling up and down, the call sequence of initState, build, and dispose is the same if you are using a ListView or a ListView.builder.

The advantage of using a ListView.builder is that widgets are created on demand, when the widget comes into view. By created I mean the constructor is called and the instance is created.

I have expanded your example and added one page using ListView and another page using ListView.builder. Additionally, I have added the static variable ItemWidget.count, which is incremented in the constructor body and stores the number of instances created.

import 'package:flutter/material.dart';

class Item {
  Item({required this.index});
  final int index;
}

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: PageOne());
  }
}

class PageOne extends StatelessWidget {
  const PageOne({super.key});

  @override
  Widget build(BuildContext context) {
    final items = List.generate(25000, (index) => Item(index: index));

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ListViewPage(items: items),
                  )),
              child: const Text('ListView'),
            ),
            ElevatedButton(
              onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ListViewBuilderPage(items: items),
                  )),
              child: const Text('ListViewBuilder'),
            ),
          ],
        ),
      ),
    );
  }
}

class ListViewPage extends StatelessWidget {
  const ListViewPage({super.key, required this.items});
  final List<Item> items;

  @override
  Widget build(BuildContext context) {
    final children = items.map((e) => ItemWidget(item: e)).toList();
    final listView = ListView(children: children);

    return Scaffold(
      body: listView,
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.navigate_before),
        onPressed: () => Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => const PageOne(),
            )),
      ),
    );
  }
}

class ListViewBuilderPage extends StatelessWidget {
  const ListViewBuilderPage({super.key, required this.items});
  final List<Item> items;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.navigate_before),
        onPressed: () => Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => const PageOne(),
            )),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) => ItemWidget(item: items[index]),
      ),
    );
  }
}

class ItemWidget extends StatefulWidget {
  ItemWidget({super.key, required this.item}) {
    ++_count;
  }
  final Item item;
  static int _count = 0;
  static int get count => _count;
  @override
  State<ItemWidget> createState() => _ItemWidgetState();
}

class _ItemWidgetState extends State<ItemWidget> {
  @override
  void initState() {
    super.initState();
    debugPrint(
        'Init state ${widget.item.index} => count: ${ItemWidget.count}');
  }

  @override
  void dispose() {
    debugPrint(
        'Dispose state ${widget.item.index} => count: ${ItemWidget.count}');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    debugPrint(
        'Build widget: ${widget.item.index} => count: ${ItemWidget.count}');
    return Text('widget index: ${widget.item.index} => count: ${ItemWidget.count}');
  }
}

Restarting the app and viewing the page ListViewPage, the following list is displayed:

widget index: 0 => count: 25000
widget index: 1 => count: 25000
widget index: 2 => count: 25000
...

Note that the constructor has already been called 25000 times.

Restarting the app and viewing the page ListViewBuilderPage, the following list is displayed:

widget index: 0 => count: 1
widget index: 1 => count: 2
widget index: 2 => count: 3
...

In this case, ItemWidgets are created on demand.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for example and response. I did some other checks and I come to this conclusion: Widget are crated at once, but state are created on demand in ListView. My confusion come due to this behaviour. What I don't understand is why state is created on demand in ListView while Stateful/StatelessWidget are created at once.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.