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

1. O Que Você Vai Aprender


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.

DART
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]);
  },
)
TEXT
> 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

100%
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]
TEXT
> 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

TEXT
> 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)

DART
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]);
        },
      ),
    );
  }
}
TEXT
> 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

TEXT
> 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)

DART
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),
    );
  },
)
TEXT
> 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

TEXT
> 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

DART
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]),
)
TEXT
> 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

TEXT
> 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

DART
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,
    );
  }
}
TEXT
> 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

TEXT
> 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

DART
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),
    ),
  ],
)
TEXT
> 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

TEXT
> 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

DART
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;
}
TEXT
> 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

DART
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'))),
            ),
          ],
        ),
      ),
    );
  }
}
TEXT
> 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: true e physics: 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_view com MasonryGridView.builder. O Flutter padrão não inclui grade escalonada.


📖 Resumo


📝 Exercícios

  1. Básico (⭐): Crie um ListView.builder com 100 itens, cada um exibindo nome e preço do produto, com itemExtent fixo.
  2. Intermediário (⭐⭐): Implemente puxar-para-atualizar + carregar-mais automaticamente ao rolar até o fundo, exibindo CircularProgressIndicator durante o carregamento.
  3. 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.

← Aula Anterior | Próxima Aula →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%