React Native: Criando Grids com FlatList

Instagram/FB, Google, Apple, qual o segredo de um bom grid?

Grids são uma escolha comum para interface, principalmente em aplicativos móveis. Mas o que torna essa escolha, uma boa escolha? Ou melhor, como implementar um Grid de uma maneira correta e consistente com dados dinâmicos? Sabe aquela última linha que só tem 1 item? Já te deu dor de cabeça né? 😢🤕

Citando as diretrizes do Material Design:

Uma lista em grid é mais adequada para apresentar dados homogêneos, tipicamente imagens, e é otimizado para compreensão visual e diferenciação entre tipos de dados semelhantes.

E no React Native, nós temos um elemento nativo, otimizado e com várias possibilidades de apresentação para esse tipo de layout.

Estou falando do FlatList.

Image for post
Image for post
Um Grid consistente com a turma do Carros!

1. Criando nosso FlatList inicial

Vamos começar criando um novo projeto React Native. Assim como no artigo anterior. Não irei falar sobre os requesitos básicos para instalar React Native na sua máquina. Para isso, eu recomendo você ir na documentação oficial e seguir o Getting Started.

Vamos iniciar um novo projeto:

react-native init FlatListGrid

Agora, vamos modificar nosso App.js e substituir seu conteúdo por:

import React from "react";
import { FlatList, SafeAreaView, StyleSheet, Text, View } from "react-native";
class App extends React.Component {
state = {
data: [
{ id: "00", name: "Relâmpago McQueen" },
{ id: "01", name: "Agente Tom Mate" },
{ id: "02", name: "Doc Hudson" },
{ id: "03", name: "Cruz Ramirez" }
]
};
render() {
return (
<SafeAreaView>
<FlatList
data={this.state.data}
keyExtractor={item => item.id}
renderItem={({ item }) => {
return (
<View style={styles.item}>
<Text style={styles.text}>{item.name}</Text>
</View>
);
}}
/>
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
item: {
alignItems: "center",
backgroundColor: "#dcda48",
flexGrow: 1,
margin: 4,
padding: 20
},
text: {
color: "#333333"
}
});
export default App;

Teremos algo como:

Image for post
Image for post
Primeira etapa, FlatList padrão.

2. Alterando o FlatList para renderizar um Grid

FlatList, por padrão, irá renderizar uma lista cima-baixo, como na imagem anterior. Como mencionado anteriormente, o estilo de lista em grid é bem comum em mobile, e por isso, FlatList tem uma prop especialmente para isso, é a numColumns .

Vamos passar um valor 3 para numColumns e ver o resultado:

...
<FlatList
data={this.state.data}
keyExtractor={item => item.id}
numColumns={3} // Número de colunas
renderItem={({ item }) => {
...
Image for post
Image for post
Ooops, as colunas foram aplicadas, mas algo errado não está certo.

Parece que algo deu errado aqui 🤔. Na verdade, não está tãããooo errado, só precisamos entender o que aconteceu aqui 🤓.

FlatList pegou o número total de elementos e os dividiu baseado no número passado para numColumns, nesse caso, 3. Como podemos ver, a primeira linha, temos 3 colunas. Porém, agora, nós temos 2 problemas aqui, e eles são diferentes.

2. Elementos transbordando o layout (Problema #1)

Na primeira linha, o conteúdo está transbordando (overflow) para fora do nosso layout. Isso é resultado do cálculo realizado pelo Flexbox.

Para calcular o espaço disponível entre 3 elementos, Flexbox irá levar em conta o conteúdo (content-box) para só então dividir o espaço disponível. Como nosso texto é grande para o espaço em questão, não tem muito espaço em sobra para dividir, e, ao levar o valor do espaço do conteúdo em conta, nosso layout transborda o espaço disponível.

Para resolver esse problema, podemos dizer ao Flexbox que o valor base do elemento, é zero 0 , e para dividir o espaço restante igualmente. Para isso, podemos usar o flexBasis e, conforme a documentação do Yoga mostra, o valor será aplicado antes de qualquer cáculo grow/shrink que o Flexbox irá realizar.

Entendo o problema, vamos alterar nossos estilos:

...
const styles = StyleSheet.create({
item: {
...
flexBasis: 0,
...

E o resultado é:

Image for post
Image for post
Aeeee!!! 🎉🎉🎉 Nossa primeira linha está correta!

E agora, a dor de cabeça de muita gente…

3. "aquela última linha com 1 item" (Problema #2)

Ao pensar na estrutura de layout do React Native e como os elementos se posicionam, eu te faço uma pergunta:

Essa última linha, o comportamento do elemento está certo ou errado?

Vou deixar você refletir alguns segundos e vou ali pegar algo para comer, um café ☕️ com cookie 🍪 (ou é bolacha? ou biscoito?)

Image for post
Image for post
Até parece uma foto minha 🤔

E aí, o que você acha?

Sem muitos detalhes de implementação, a combinação de FlatList e a prop numColumns > 1 , irá criar um novo elemento para o conjunto de N (onde N é o número passado) e colocar esse conjunto dentro desse novo elemento.

Lembra daquele ifzão que você já fez na view do WordPress ou Rails que, a cada 3 blog post você colocava um .container-articles só para adicionar aqueles estilos desgraç…maneiros do Designer? Então, same old same old.

Deu risada né? Flanders tá vendo! 😂😂😂

Image for post
Image for post

Na própria documentação do React Native, mostra como podemos adicionar estilos para esse elemento através da columnWrapperStyle, se necessário.

Então, respondendo a pergunta anterior:

Essa última linha, o comportamente do elemento está certo ou errado?

Ele está, meh, funcionando corretamente. Como os elementos em React Native são baseados em Flexbox, e o conjunto em que esse elemento se encontra, só existe 1, o seu flexGrow irá ocupar todo o espaço disponível, ocupando a linha inteira.

Então, qual é o pulo-do-gato aqui? Nós precisamos preencher os elementos que estão em falta e adicionar estilos para escondê-los visualmente. (Vai dizer que isso não soa como aquele hackzão CSS? 😂🙈)

Para isso, iremos alterar como estamos passando os elementos para a prop data do nosso componente FlatList. Também iremos mover o número 3 para uma constante, atualizando nosso render para:

...
render() {
const columns = 3;
return (
<SafeAreaView>
<FlatList
data={createRows(this.state.data, columns)}
keyExtractor={item => item.id}
numColumns={columns}
...

createRows é uma função que irá retornar um array onde o módulo do número de colunas sempre será zero.

What?… Vamos ao código, e explico mais tarde:

function createRows(data, columns) {
const rows = Math.floor(data.length / columns); // [A]
let lastRowElements = data.length - rows * columns; // [B]
while (lastRowElements !== columns) { // [C]
data.push({ // [D]
id: `empty-${lastRowElements}`,
name: `empty-${lastRowElements}`,
empty: true
});
lastRowElements += 1; // [E]
}
return data; // [F]
}

A raíz do problema é que, o resto da divisão (módulo) do número de colunas desejado pelo número de dados disponíveis (o array), está contendo "sobras", ou seja:

[1, 2, 3, 4].length % 3 = 1

Para resolver isso em createRows nós estamos:

  • [A]: Calculando o número base de linhas que teremos
  • [B]: Calculando a quantidade de itens que irá sobrar na última linha
  • [C]: Enquanto o número de itens na última linha não for igual ao número desejado de colunas
  • [D]: Iremos adicionar elementos vazios no array disponibilizado
  • [E]: Incrementamos o contador
  • [F]: Retornamos o novo array preenchido

E o resultado é:

Image for post
Image for post
Quase lá, não vale chamar de gambiarra!

E para finalizar, vamos alterar o renderItem para detectar o elemento vazio:

renderItem={({ item }) => {
if (item.empty) {
return <View style={[styles.item, styles.itemEmpty]} />;
}
return (
<View style={styles.item}>
<Text style={styles.text}>{item.name}</Text>
</View>
);
}}

E adicionar os estilos para styles.itemEmpty :

...
itemEmpty: {
backgroundColor: "transparent"
},
...

E o resultado final:

Image for post
Image for post
Agora sim parece um Grid consistente!

Vale lembrar que, usamos style={[styles.item, styles.itemEmpty]} porque os elementos precisam ter o mesmo espaçamento (margin, padding, etc) que os outros, o truque aqui é deixar o background/texto da mesma cor que o plano de fundo.

Finalizando

Com isso, concluímos a criação de um Grid consistente e seguindo as boas práticas de UX/UI para esse tipo de interface.

O código de exemplo complete pode ser encontrado nesse repositório.

A tecnologia e framework mudam, mas a solução…😝

Créditos ⭐️

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store