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}');
}
}
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, ItemWidget
s are created on demand.