Erros comuns com ListViews no Flutter

Tempo de leitura: 4 minutes

Algumas coisas que devem ser evitadas para manter a rolagem suave

1. Encolher o ListView.builder ou usar o NeverScrollableScrollPhysics.

A principal vantagem de usar o ListView.builder é seu mecanismo de otimização, que inicializa apenas os itens necessários para serem exibidos na tela. Isso faz com que ele funcione sem problemas com milhares de itens, da mesma forma que faria com uma dúzia.

E se a lista que queremos exibir precisar ser incorporada em outra exibição de rolagem? Um erro comum é desativar a rolagem adicionando NeverScrollableScrollPhysics ou definindo shrinkWrap como true. Essa abordagem inicializa todos os itens de uma vez, o que pode ser problemático. Podemos realizar um experimento simples para confirmar esse problema.

SingleChildScrollView(
  child: Column(
    children: [
      const Card(child: Text("Header card")),
      ListView.builder(
        physics: const NeverScrollableScrollPhysics(),
        itemCount: 1000,
        shrinkWrap: true,
        itemBuilder: (context, index) {
          print("building item #${index}");
          return Card(child: Text(index.toString()));
        },
      ),
      const Card(child: Text("Footer card")),
    ],
  ),
)

Como podemos ver no resultado, todos os itens são inicializados simultaneamente, o que leva a uma perda significativa de desempenho. Isso acontece porque a lista inteira é renderizada de uma vez, em vez de apenas a parte visível.

Então, o que devemos fazer em vez disso? A abordagem recomendada é usar Slivers. Os slivers permitem uma renderização mais eficiente ao criar apenas os itens que estão visíveis no momento na tela. Isso ajuda a manter um desempenho suave, mesmo com listas grandes.

CustomScrollView(
  slivers: [
    const SliverToBoxAdapter(child: Card(child: Text("Header card"))),
    SliverList.builder(
      itemBuilder: (context, index) {
        print("building item #${index}");
        return Card(child: Text(index.toString()));
      },
    ),
    const SliverToBoxAdapter(child: Card(child: Text("Footer card"))),
  ],
)

Como podemos ver, ele inicializa apenas os itens que devem ser mostrados na tela, além de um pouco mais devido ao cache da janela de visualização. É necessário ter alguns itens já pré-inicializados, de modo que não haja tremores quando o usuário começar a rolar a tela. A quantidade de pixels a ser armazenada em cache é configurável pela propriedade cacheExtent do CustomScrollView ou do ListView e o padrão é 250.

 

2. Permitir que cada item da lista determine a altura por conta própria.

Uma das operações mais caras na renderização da interface do usuário do Flutter é o cálculo dos tamanhos dos widgets. Ao desenhar listas, há maneiras de evitar cálculos excessivos, especialmente para listas em que todos os itens têm a mesma altura.

Você pode usar duas propriedades para isso:

  1. prototypeItem – um widget cuja altura será usada para cada item da lista.
  2. itemExtent – um valor numérico que especifica a altura de cada item.

Muitos tutoriais recomendam o uso do itemExtent, mas ele tem algumas desvantagens que muitas vezes são ignoradas. Se você fornecer um valor constante e os itens da lista contiverem texto, o layout poderá ser interrompido quando o usuário aumentar o tamanho da fonte nas configurações do sistema operacional.

Para evitar isso, você pode calcular o tamanho real dos itens multiplicando o tamanho da fonte pela escala de texto do sistema operacional e adicionando preenchimentos. Isso pode se tornar problemático e difícil de manter. Há uma maneira melhor de fazer isso:

ListView.builder(
  itemCount: 1000,
  prototypeItem: const Card(child: Text("")),
  itemBuilder: (context, index) {
    return Card(child: Text(index.toString()));
  },
)

No caso das listas de fitas, também temos a possibilidade de fornecer um protótipo:

SliverPrototypeExtentList.builder(
  itemBuilder: (context, index) {
    return Card(child: Text(index.toString()));
  },
  prototypeItem: const Card(child: Text("")),
),

Observe que o ListView.separator não tem essas propriedades; considere a possibilidade de adicionar um divisor aos itens da lista e usar o ListView.builder em vez disso.

 

3. Envolvimento de um ListView em um widget Padding

Se você quiser adicionar preenchimento a um widget ListView (isso também se aplica ao SingleChildScrollView), certifique-se de não envolver um ListView em um widget Padding. Isso funcionará como uma margem para o conteúdo rolável em vez de preenchimento. Em vez disso, adicione o parâmetro padding diretamente ao seu objeto ListView (ou SingleChildScrollView).

 ListView.builder(
  itemCount: 1000,
  padding: const EdgeInsets.all(40),
  prototypeItem: const Card(child: Text("")),
  itemBuilder: (context, index) {
    return Card(child: Text(index.toString()));
  },
)

A diferença pode ser vista aqui:

Infelizmente, nem o SliverLists nem o CustomScrollView têm esse parâmetro. Nesse caso, para adicionar algum preenchimento ao conteúdo de um CustomScrollView, você pode usar o widget SliverPadding:

 SliverPadding(
  padding: EdgeInsets.all(20),
  sliver: SliverPrototypeExtentList.builder(
    itemBuilder: (context, index) {
      return Card(child: Text(index.toString()));
    },
    prototypeItem: const Card(child: Text("")),
  ),
),

 

4. Uso de scroll physics incorreta para plataformas diferentes

O Android e o iOS têm diferentes físicas de rolagem e comportamento de rolagem excessiva. Aqui está uma citação da documentação:

O Android e o iOS têm simulações complexas de física de rolagem que são difíceis de descrever verbalmente. Em geral, a rolagem do iOS tem mais peso e atrito dinâmico, mas o Android tem mais atrito estático. Portanto, o iOS ganha alta velocidade de forma mais gradual, mas para de forma menos abrupta e é mais escorregadio em velocidades baixas.

Quando você define explicitamente o physics como BouncingScrollPhysics ou ClampingScrollPhysics, ela parecerá nativa apenas em uma plataforma. Use apenas AlwaysScrollableScrollPhysics, a menos que seja um caso raro em que você realmente precise ter a física de rolagem definida dessa forma.