Kodokon kodokon.com

Высокопроизводительные списки с ListView.builder

Отображай списки из тысяч элементов без тормозов благодаря ListView.builder, ListTile и элементам, вынесенным в отдельные виджеты.

8 мин · 3 вопросов

Открыть этот урок в Kodokon

Настоящее приложение почти всегда показывает списки: сообщения, товары, контакты. Уже знакомый тебе конструктор ListView(children: ...) строит все дочерние виджеты сразу - нормально для десяти элементов, катастрофа для тысячи. ListView.builder строит только элементы рядом с видимой областью, по мере необходимости, пока ты прокручиваешь.

DART
import 'package:flutter/material.dart';

class ContactsScreen extends StatelessWidget {
  const ContactsScreen({super.key, required this.names});

  final List<String> names;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contacts')),
      body: ListView.builder(
        itemCount: names.length,
        itemBuilder: (context, index) {
          return ListTile(
            leading: const Icon(Icons.person),
            title: Text(names[index]),
          );
        },
      ),
    );
  }
}
Строятся только видимые элементы, даже если имён 10 000.

Весь механизм держится на двух параметрах: itemCount объявляет общее количество элементов, а itemBuilder создаёт элемент по заданному индексу, только когда тот приближается к экрану. За содержимое каждой строки отвечает ListTile, твой союзник из Material: leading, title, subtitle и trailing задают структуру строки, а onTap добавляет чернильную волну при касании. Но как только элемент разрастается, не давай ему раздуваться внутри itemBuilder: вынеси его в отдельный виджет.

DART
import 'package:flutter/material.dart';

class ContactTile extends StatelessWidget {
  const ContactTile({
    super.key,
    required this.name,
    required this.onTap,
  });

  final String name;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CircleAvatar(child: Text(name[0])),
      title: Text(name),
      trailing: const Icon(Icons.chevron_right),
      onTap: onTap,
    );
  }
}
Элемент списка, превращённый в переиспользуемый и тестируемый виджет.

Вынесенный элемент можно тестировать отдельно, переиспользовать на других экранах, и он делает itemBuilder тривиальным. Остаётся одна частая потребность: разделители. ListView.separated работает как builder, но с третьим параметром separatorBuilder, который вызывается между каждой парой элементов - после последнего элемента разделителя никогда не будет.

DART
import 'package:flutter/material.dart';

class ContactList extends StatelessWidget {
  const ContactList({super.key, required this.names});

  final List<String> names;

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: names.length,
      separatorBuilder: (context, index) {
        return const Divider(height: 1);
      },
      itemBuilder: (context, index) {
        return ContactTile(
          name: names[index],
          onTap: () => debugPrint(names[index]),
        );
      },
    );
  }
}
separatorBuilder вставляет Divider между каждой парой элементов.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Почему для списка из 500 элементов стоит предпочесть ListView.builder вместо ListView(children: ...)?
    • Он автоматически сортирует элементы
    • Он строит только видимые элементы, по мере необходимости
    • Он кэширует изображения
    • Он автоматически добавляет разделители
  2. Какой конструктор вставляет виджет между каждой парой элементов?
    • ListView.divided
    • ListView.builder
    • ListView.separated
  3. В чём польза от выноса элемента списка в отдельный класс-виджет?
    • Это обязательно, иначе ListView.builder не скомпилируется
    • Это открывает const, изолирует перестроения и упрощает переиспользование
    • Это делает список прокручиваемым
    • Это избавляет от написания itemCount