Flutter: Lista e Rolagem
Rolagem é a interação mais natural no mobile — se sua lista não é suave, os usuários deslizarão em direção ao botão de desinstalar.
📋 Pré-requisitos: Você precisa dominar o seguinte primeiro
- Aula 6: Material Design e Widgets Comuns
1. O Que Você Vai Aprender
- ListView.builder / ListView.separated carregamento preguiçoso e otimização de desempenho com itemExtent
- GridView.builder / SliverGrid renderização de grade com grandes volumes de dados
- ScrollController: escuta de rolagem, voltar ao topo, puxar para carregar mais
- Família Sliver: SliverAppBar, SliverList, SliverPersistentHeader
- Lista de produtos de milhão do ShopApp + pull-to-refresh + rolagem infinita
2. Uma História Real de uma Lista Lenta
(1) O Problema: O Pesadelo dos 30fps com uma Lista de Milhões de Produtos
O ShopApp do Bob tem milhões de registros de produtos. Ele usa ListView(children: allProducts.map(...).toList()) para criar todos os widgets de uma vez — 100 itens cai para 30fps, 1000 itens causa um crash por OOM. Pior ainda, os usuários precisam tocar manualmente em "Carregar Mais" na parte inferior, resultando em uma taxa de rejeição de 60%.
(2) A Solução com Carregamento Preguiçoso + Rolagem Infinita
ListView.builder apenas constrói os itens visíveis — mesmo com 10 milhões de registros, renderiza apenas os 10-20 itens visíveis na tela. Combinado com ScrollController para monitorar a posição de rolagem, aciona automaticamente o carregamento de mais dados.
import 'package:flutter/material.dart';
// Simplified class definition
class Product {
final String name;
final double price;
const Product({required this.name, required this.price});
}
// Lazy loading: only builds visible items
ListView.builder(
itemCount: products.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == products.length) return const LoadingIndicator();
return ProductTile(product: products[index]);
},
)
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Benefício: 60fps + Carregamento Sem Interrupções
Após mudar para carregamento preguiçoso com builder + rolagem infinita, a lista de milhões de produtos do Bob roda a 60fps estáveis, e a taxa de rejeição cai de 60% para 15%.
3. O Sistema ListView
graph TD
SV[ScrollView] --> LV[ListView]
SV --> GV2[GridView]
SV --> CS[CustomScrollView]
CS --> SA[SliverAppBar]
CS --> SL[SliverList]
CS --> SG[SliverGrid]
CS --> SPH[SliverPersistentHeader]
LV --> |builder| Lazy[Lazy Loading]
LV --> |controller| LoadMore[Load More]
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(1) Métodos de Construção do ListView
| Construtor | itemCount | Momento do Build | Caso de Uso |
|---|---|---|---|
ListView(children:) |
Fixo | Construir todos de uma vez | Poucos itens (<20) |
ListView.builder() |
Variável | Carregamento preguiçoso dos itens visíveis | Listas com grandes volumes de dados |
ListView.separated() |
Variável | Carregamento preguiçoso + separadores | Listas com divisores |
ListView.custom() |
Variável | SliverChildDelegate personalizado | Requisitos especiais |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Lista de produtos (ListView.builder)
import 'package:flutter/material.dart';
// Simplified class definition
class Product {
final String name;
final double price;
final int id;
const Product({required this.name, required this.price, required this.id});
}
// Simplified API class
class ProductApi {
Future<List<Product>> fetchProducts({int page = 1}) async {
await Future.delayed(const Duration(milliseconds: 500));
return List.generate(20, (i) => Product(
name: 'Product ${(page - 1) * 20 + i + 1}',
price: 9.99 + i * 5,
id: (page - 1) * 20 + i + 1,
));
}
}
final api = ProductApi();
class ProductListPage extends StatefulWidget {
const ProductListPage({super.key});
@override
State<ProductListPage> createState() => _ProductListPageState();
}
class _ProductListPageState extends State<ProductListPage> {
final ScrollController _controller = ScrollController();
List<Product> _products = [];
bool _isLoading = false;
bool _hasMore = true;
int _page = 1;
@override
void initState() {
super.initState();
_loadProducts();
_controller.addListener(_onScroll);
}
void _onScroll() {
if (_controller.position.pixels >= _controller.position.maxScrollExtent - 200) {
_loadProducts(); // Load more when near bottom
}
}
Future<void> _loadProducts() async {
if (_isLoading || !_hasMore) return;
setState(() => _isLoading = true);
final newProducts = await api.fetchProducts(page: _page++);
setState(() {
_products.addAll(newProducts);
_isLoading = false;
_hasMore = newProducts.length >= 20;
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
_page = 1;
_products.clear();
_hasMore = true;
await _loadProducts();
},
child: ListView.builder(
controller: _controller,
itemCount: _products.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == _products.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
return ProductListTile(product: _products[index]);
},
),
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Lista com divisores (ListView.separated)
import 'package:flutter/material.dart';
// Simplified class definition
class Order {
final int id;
final double total;
final List<String> items;
final String status;
const Order({required this.id, required this.total, required this.items, required this.status});
}
Widget _buildStatusChip(String status) => Chip(label: Text(status));
ListView.separated(
itemCount: orders.length,
separatorBuilder: (context, index) => const Divider(height: 1, indent: 72),
itemBuilder: (context, index) {
final order = orders[index];
return ListTile(
leading: CircleAvatar(child: Text('#${order.id}')),
title: Text('Order #${order.id}'),
subtitle: Text('\$${order.total.toStringAsFixed(2)} • ${order.items.length} items'),
trailing: _buildStatusChip(order.status),
);
},
)
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
4. Otimização de Desempenho
(1) itemExtent e prototypeItem
| Otimização | Efeito | Melhoria |
|---|---|---|
itemExtent |
Altura fixa do item, pula medição | Desempenho de rolagem +30% |
prototypeItem |
Medir item de referência uma vez | Quando todos os itens têm altura uniforme |
Construtores const |
Reutilizar instâncias de widget | Reduz overhead de reconstrução |
addAutomaticKeepAlives |
Controlar política de cache | Otimização de memória |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Otimização com itemExtent
import 'package:flutter/material.dart';
// Fixed-height items: use itemExtent for maximum performance
ListView.builder(
itemExtent: 72, // Each item is exactly 72 pixels tall
itemCount: 10000,
itemBuilder: (context, index) => ListTile(
title: Text('Product ${index + 1}'),
trailing: Text('\$${(9.99 + index * 5).toStringAsFixed(2)}'),
),
)
// Variable-height items: use prototypeItem
ListView.builder(
prototypeItem: const ListTile(title: Text('Prototype'), trailing: Text('\$0.00')),
itemCount: products.length,
itemBuilder: (context, index) => ProductListTile(product: products[index]),
)
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
5. Controle Avançado com ScrollController
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Botão de voltar ao topo
import 'package:flutter/material.dart';
class ScrollToTopList extends StatefulWidget {
const ScrollToTopList({super.key});
@override
State<ScrollToTopList> createState() => _ScrollToTopListState();
}
class _ScrollToTopListState extends State<ScrollToTopList> {
final _controller = ScrollController();
bool _showTopButton = false;
@override
void initState() {
super.initState();
_controller.addListener(() {
final show = _controller.offset > 500;
if (show != _showTopButton) setState(() => _showTopButton = show);
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _controller,
itemCount: 5000,
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
),
floatingActionButton: _showTopButton
? FloatingActionButton.mini(
onPressed: () => _controller.animateTo(0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut),
child: const Icon(Icons.arrow_upward),
)
: null,
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
6. Rolagem Avançada com Sliver
Slivers são blocos de construção do CustomScrollView que podem ser combinados para criar efeitos de rolagem complexos.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: CustomScrollView + SliverAppBar
import 'package:flutter/material.dart';
// Simplified class definitions
class Product {
final String name;
const Product({required this.name});
}
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override Widget build(BuildContext context) => Card(child: Text(product.name));
}
final categories = ['All', 'Electronics', 'Clothing', 'Home'];
final products = List.generate(20, (i) => Product(name: 'Product ${i + 1}'));
const _hasMore = true;
class LoadingIndicator extends StatelessWidget {
const LoadingIndicator({super.key});
@override Widget build(BuildContext context) => const Padding(
padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()));
}
CustomScrollView(
slivers: [
// Collapsible header
SliverAppBar(
expandedHeight: 200,
floating: true,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Flash Sale'),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue, Colors.purple]),
),
child: const Center(child: Text('UP TO 50% OFF',
style: TextStyle(fontSize: 32, color: Colors.white, fontWeight: FontWeight.bold))),
),
),
),
// Category chips (fixed height)
SliverToBoxAdapter(
child: SizedBox(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
children: categories.map((c) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Chip(label: Text(c)),
)).toList(),
),
),
),
// Product grid
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid.extent(
maxCrossAxisExtent: 180,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: products.map((p) => ProductCard(product: p)).toList(),
),
),
// Loading indicator at bottom
SliverToBoxAdapter(
child: _hasMore ? const LoadingIndicator() : const SizedBox(height: 40),
),
],
)
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Cabeçalho de categoria fixo com SliverPersistentHeader
import 'package:flutter/material.dart';
class StickyCategoryHeader extends SliverPersistentHeaderDelegate {
final String category;
StickyCategoryHeader(this.category);
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
alignment: Alignment.centerLeft,
child: Text(category, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
);
}
@override
double get maxExtent => 40;
@override
double get minExtent => 40;
@override
bool shouldRebuild(covariant StickyCategoryHeader oldDelegate) =>
oldDelegate.category != category;
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
7. Exemplo Completo: Lista de Produtos com Rolagem Infinita do ShopApp
import 'package:flutter/material.dart';
// Simplified class definitions
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override Widget build(BuildContext context) => Card(child: Padding(
padding: const EdgeInsets.all(8), child: Text('${product.name} \$${product.price}')));
}
class MockProducts {
static List<Product> generate({int count = 20, int offset = 0, String category = 'All'}) {
return List.generate(count, (i) => Product(
id: offset + i + 1,
name: 'Product ${offset + i + 1}',
price: (9.99 + (offset + i) * 5).toDouble(),
));
}
}
class InfiniteProductList extends StatefulWidget {
const InfiniteProductList({super.key});
@override
State<InfiniteProductList> createState() => _InfiniteProductListState();
}
class _InfiniteProductListState extends State<InfiniteProductList> {
final _controller = ScrollController();
final List<Product> _products = [];
bool _isLoading = false;
bool _hasMore = true;
int _page = 0;
String _category = 'All';
@override
void initState() {
super.initState();
_loadMore();
_controller.addListener(() {
if (_controller.position.pixels >= _controller.position.maxScrollExtent - 300 && !_isLoading && _hasMore) {
_loadMore();
}
});
}
Future<void> _loadMore() async {
if (_isLoading) return;
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 1)); // Simulate API
final newItems = MockProducts.generate(count: 20, offset: _page * 20, category: _category);
setState(() {
_products.addAll(newItems);
_page++;
_isLoading = false;
_hasMore = _page < 500; // Max 10,000 items for demo
});
}
Future<void> _refresh() async {
setState(() { _page = 0; _products.clear(); _hasMore = true; });
await _loadMore();
}
@override
void dispose() { _controller.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: RefreshIndicator(
onRefresh: _refresh,
child: CustomScrollView(
controller: _controller,
slivers: [
SliverToBoxAdapter(
child: SizedBox(height: 48, child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
children: ['All', 'Electronics', 'Clothing', 'Home'].map((c) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ChoiceChip(label: Text(c), selected: c == _category,
onSelected: (_) => setState(() { _category = c; _refresh(); })),
)).toList(),
)),
),
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid.extent(
maxCrossAxisExtent: 180,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: _products.map((p) => ProductCard(
key: ValueKey(p.id), product: p,
)).toList(),
),
),
if (_isLoading) const SliverToBoxAdapter(
child: Padding(padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator())),
),
if (!_hasMore) const SliverToBoxAdapter(
child: Padding(padding: EdgeInsets.all(16), child: Center(child: Text('No more products'))),
),
],
),
),
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
❓ Perguntas Frequentes
P: Qual a diferença entre ListView e SingleChildScrollView? R: ListView carrega preguiçosamente para listas; SingleChildScrollView constrói todos os filhos de uma vez, adequado para páginas longas com pouco conteúdo.
P: ListView aninhado dentro de ListView gera erro? R: Defina o ListView interno como
shrinkWrap: trueephysics: NeverScrollableScrollPhysics(). No entanto, isso prejudica o desempenho — considere usar Slivers em vez disso.
P: RefreshIndicator não funciona? R: O filho deve ser rolável (ListView/GridView/CustomScrollView), e o conteúdo deve exceder a altura da viewport.
P: maxScrollExtent do ScrollController é 0? R: maxScrollExtent é 0 quando o conteúdo não excede a viewport. Garanta que a lista seja longa o suficiente, ou leia em WidgetsBinding.instance.addPostFrameCallback.
P: Qual a diferença entre floating e snap do SliverAppBar? R: floating=true mostra imediatamente ao rolar para baixo; snap=true expande completamente ao soltar (requer floating).
P: Como implementar uma grade escalonada (MasonryGrid)? R: Use o pacote
flutter_staggered_grid_viewcomMasonryGridView.builder. O Flutter padrão não inclui grade escalonada.
📖 Resumo
- ListView.builder carrega preguiçosamente, apenas construindo itens visíveis, suportando dados de nível de milhão
- itemExtent fixa a altura do item, pulando medição, melhorando o desempenho de rolagem em 30%+
- ScrollController monitora a rolagem, controla voltar ao topo, e aciona carregar-mais
- RefreshIndicator implementa puxar-para-atualizar
- Combinações CustomScrollView + Sliver criam efeitos de rolagem complexos
📝 Exercícios
- Básico (⭐): Crie um ListView.builder com 100 itens, cada um exibindo nome e preço do produto, com itemExtent fixo.
- Intermediário (⭐⭐): Implemente puxar-para-atualizar + carregar-mais automaticamente ao rolar até o fundo, exibindo CircularProgressIndicator durante o carregamento.
- Desafio (⭐⭐⭐): Use CustomScrollView + SliverAppBar + SliverGrid para construir uma página inicial: cabeçalho colapsável + tags de categoria + grade de produtos + rolar-para-carregar-mais.