flutterperformancememoryflutter-listview

Why ListView create a new object if it is already in memory


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}');
  }
}

Solution

  • 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.