💎 Flutter: Comece a animar suas listas
Muitas vezes deixamos de adicionar animações em alguns componentes da interface por questão de complexidade, justificando ser um esforço desnecessário e com valor agregado baixo, mas não é bem assim.
Nossos olhos estão acostumados com movimentos orgânicos, e ver um item saindo do ponto A para o ponto B ou até mesmo sumindo da tela, sem qualquer transição indicando o que aconteceu, vai nos deixar realmente confusos.
Isso se agrava quando se trata de uma lista de objetos, já que ali temos muito mais informações com as quais nos preocupar, e quanto mais dinâmico isso fica, pior é a experiência na utilização.
O que são Animated Lists
Como tudo no Flutter, é mais um widget, mas não qualquer widget. A AnimatedList faz parte do grupo de widgets que auxiliam na construção de comportamentos de transição no flutter, como por exemplo, o AnimatedContainer, AnimatedCrossFade, AnimatedOpacity, e muitos outros, que podemos abordar em um outro momento.
// just another widget
AnimatedList();
Um dos pontos que faz o Flutter ser tão poderoso na construção de verdadeiras experiências, é justamente a disponibilidade dessas ferramentas de animação pug and play, que nos permitem criar transições de forma simplificada, ou customizar de maneira mais complexa se necessário.
Funcionamento principal
A AnimatedList tem um funcionamento parecido com a ListView.builder, que cria um scroll na tela estimando a quantidade de elementos que serão necessários durante o build.
Porém, a forma como o AnimatedList decide se vai fazer o build de um item ou não é um pouco diferente, por isso a propriedade que recebe o tamanho da lista se chama initialItemCount
, e não itemCount
como na ListView.builder.
Essa propriedade vai permitir que a AnimatedList configure o que vamos chamar de "lista fantasma”, onde as posições dos itens vão servir como referência para que a lista decida quais itens precisam de um novo build, e qual será a animação.
Vamos implementar um exemplo
Por enquanto, vamos renderizar apenas um card ao abrir a página, sem a possibilidade de alterar a lista ou animar de alguma forma:
class _MyHomePageState extends State<MyHomePage> {
// Através da key vamos poder manipular a lista fantasma
final _listKey = GlobalKey<AnimatedListState>();
// A lista real, que será utilizada para construir os itens
final _myList = <String>['title1', 'title2', 'title3'];
// Funções para manipular as listas
_addNewItem() {}
_removeItem(int index) {}
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(
// Sua animated list na àrvore de widgets
child: AnimatedList(
// Key única para cada lista
key: _listKey,
// Tamanho da lista real
initialItemCount: _myList.length,
// Build dos itens
itemBuilder: (context, index, animation) {
// Nosso item, sem animação por enquanto
return MyCard(
onTap: () => _removeItem(index),
title: _myList[index],
);
},
),
),
);
}
Itens da nossa lista
Agora vamos criar também um widget que representará o nosso item, será chamado de MyCard
. Neste exemplo vai receber um título, uma cor e um callback de click:
class MyCard extends StatelessWidget {
final String title;
final Color color;
final VoidCallback? onTap;
const MyCard({
super.key,
required this.title,
this.onTap,
this.color = Colors.purpleAccent,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 100,
width: double.infinity,
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
color: color,
child: Center(
child: Text(
title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
);
}
}
Nosso resultado até agora é uma lista simples, que poderia ter sido criada por uma ListView ou até mesmo por uma Column. Mas a partir dela começa a ficar mais interessante.
Criando uma animação
No momento em que a AnimatedList faz o build de um item, é chamada uma função (itemBuilder) que recebe as seguintes propriedades:
context
(BuildContext)index
(int)animation
(Animation)
Com um Animation<double>
disponível, podemos adicionar um widget de Transition dentro do build, que será um widget pai do item que estamos pretendendo renderizar.
O Flutter disponibiliza diversos widgets com essa premissa, como: SizeTransition, ScaleTransition ou FadeTransition por exemplo.
Todos possuem a propriedade
child
, para receber o widget que será afetado visualmente sempre que a propriedade de animação mudar.
Vamos utilizar um SizeTransition por enquanto:
...
child: AnimatedList(
key: _listKey,
initialItemCount: _myList.length,
itemBuilder: (context, index, animation) {
// A AnimatedList controla a transição do item
// - Para adicionar: ela amplia o animation em ordem crescente
// - Para remover: ela reduz de forma decrescente
return SizeTransition(
sizeFactor: animation,
child: MyCard(
title: _myList[index],
),
);
},
),
...
Adicionando e removendo itens
Uma coisa importante para se lembrar, é que sempre que novos itens forem inseridos ou removidos da lista real vamos precisar avisar a AnimatedList, para que ela possa manipular sua lista fantasma no index
correto, isso é feito através do _listKey.currentState
.
Podemos criar então um botão, que vai fazer o papel de incluir novos itens na nossa lista, e também avisar o currentState.
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(
// Adicionando uma coluna
child: Column(
children: [
const SizedBox(height: 50),
// Nosso card também será o botão
MyCard(
title: 'Add new item',
color: Colors.orange,
onTap: _addNewItem,
),
// Sempre expanda listas dentro de Columns
Expanded(
child: AnimatedList(
...
),
),
],
),
),
);
Adicionar
Agora é simples, para fazer o funcionamento deste botão, só precisamos utilizar a nossa função _addNewItem
e inserir algo em ambas as listas (a real, e a fantasma).
Para inserir itens na lista fantasma, utilizamos o método .insertItem()
, e nessa lista você deve sempre inserir um item por vez.
_addNewItem() {
final index = _myList.length;
final intem = 'title${index + 1}';
_listKey.currentState?.insertItem(index);
_myList.insert(index, intem);
}
Remover
Para remover um item vamos utilizar a função .removeItem()
, e nela existe uma pequena diferença: além do index do item que será removido, vamos passar também um builder, exatamente como o que passamos para a AnimatedList através da propriedade itemBuilder
.
_removeItem(int index) {
final title = _myList[index];
_listKey.currentState?.removeItem(
index,
(context, animation) => SizeTransition(
sizeFactor: animation,
child: MyCard(
title: title,
),
),
);
_myList.removeAt(index);
}
Resultado
Depois disso, o céu é o limite, existem milhares de formas de explorar esse comportamento nos seus projetos, utilizando diversas transições (inclusive ao mesmo tempo) e alterando comportamentos como curva e duração.
Animando o primeiro build
Pra finalizar, utilizando os métodos que criamos aqui, para exibir uma animação inicial nos itens você só precisa fazer 2 coisas:
- Dizer para a AnimatedList iniciar sem nenhum item na lista fantasma, apenas colocando 0 na propriedade
initialItemCount
. - Adicionar os itens após a renderização da página, isso pode ser feito chamando o
addPostFrameCallback
dentro do seuinitState
.
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
for (var i = 0; i < _myList.length; i++) {
_listKey.currentState?.insertItem(i);
await Future.delayed(const Duration(milliseconds: 500));
}
});
}
...
AnimatedList(
initialItemCount: 0,
...
)
Desta forma, essa será nossa tela após o primeiro build:
💛 Muito obrigado
Me avise se encontrar algo que eu precise corrigir ou melhorar. Espero que eu tenha contribuído com esse texto de alguma forma, sinta-se livre para deixar um comentário ou sugestão.
Me acompanhe no Twitter (@juniorlisboa29), estou começando a falar de tecnologia por lá também. Um abraço, e até a próxima! 🚀