Explorando coleções em Dart
Se você já chamou add(), addAll(), map() ou toList() para montar uma lista ou um map, talvez queira conhecer collection if, collection for e spreads…
Se você já chamou add(), addAll(), map() ou toList()
para montar uma lista ou um map, talvez queira conhecer collection if, collection for
e spreads. No ano passado, o Dart adicionou esses recursos na versão 2.3.
Neste artigo, vamos ver as coleções, explorar esses novos recursos e conferir alguns exemplos interessantes. Ao dominar esses recursos, você pode deixar seu código mais sucinto e mais fácil de ler.
Coleções
#Primeiro, precisamos entender o que é uma coleção. Uma coleção é um objeto que contém outros objetos. Por exemplo:
-
List: uma coleção ordenada de objetos com um tamanho (também chamada de array)
Set: uma coleção não ordenada de objetos únicos
Map: uma coleção não ordenada de pares chave-valor
-
Queue: uma coleção ordenada que pode adicionar/remover objetos em ambas as extremidades
-
SplayTreeMap: uma coleção ordenada de pares chave-valor baseada em uma árvore binária autobalanceada
Esses tipos estão disponíveis no pacote dart:collection. Para ainda mais tipos de coleção, confira o package:collection no pub.dev.
Cada um desses tipos de coleção implementa o Iterable, que oferece comportamentos comuns, como executar uma função em cada objeto da coleção, obter o primeiro objeto, determinar o tamanho da coleção e muito mais.
Literais de coleção
#
O Dart oferece sintaxe para construir três tipos de coleção: literais de lista ([]), literais de map ({}) e literais de set (também
{}).
Aqui está um literal de lista:
List<String> getArtists() {
return [
'Picasso',
'Warhol',
'Monet',
];
}
Aqui está um literal de map:
Map<String, String> getArtistsByPainting {
return {
'The Old Guitarist': 'Picasso',
'Orange Prince': 'Warhol',
'The Water Lily Pond': 'Monet',
};
}
E aqui está um literal de set, adicionado no Dart 2.3:
Set<String> getArtistsSet() {
return {
'Picasso',
'Warhol',
'Monet',
};
}
Se você está se perguntando por que maps e sets podem usar a mesma sintaxe {}, é porque o Dart usa
inferência de tipos para diferenciá-los. O sistema de tipos determina o tipo com base no tipo dos parâmetros
a e b. Geralmente, ele consegue determinar isso com base no conteúdo — por exemplo,
{1} é claramente um Set, e {1: 2} é claramente um Map.
Nota: Usar
{}constrói um map por padrão. Para criar um set, você pode usar uma anotação de tipo genérico:<String>{}. Usando dois parâmetros de tipo genérico, cria-se um map:<String, String>{}.
Tipos de elementos
#Cada item em um literal de coleção geralmente é um valor ou uma expressão, mas também pode ser um destes novos recursos: um collection if, um collection for ou um spread. Todos eles são chamados de elementos.
Cada elemento desempacota zero ou mais itens e os coloca na coleção que o envolve. Por exemplo, um literal de string (digamos,
"oatmeal") resulta em um item, mas um collection for desempacota 0 ou mais itens. Esses recursos também podem ser combinados de formas interessantes, como vamos explorar a seguir.
Spreads
#Um spread pega uma coleção (por exemplo, uma lista) e coloca o conteúdo dela na coleção que o envolve:
List<String> combineLists(List<String> a, List<String> b) {
return [
...a,
...b,
];
}
O código anterior é equivalente a este:
List<String> combineLists(List<String> a, List<String> b) {
var list = [];
list.addAll(a);
list.addAll(b);
return list;
}
Você também pode usar spreads em literais de map e de set:
Map<String, String> combineMaps(Map<String, String> a, Map<String, String> b) {
return {
...a,
...b,
};
}
Set<String> combineSets(Set<String> a, Set<String> b) {
return {
...a,
...b,
};
}
Tanto em maps quanto em sets, o conteúdo de b sobrescreve o conteúdo de a quando há conflito. Por exemplo, chamar
combineMaps({'foo': 'bar'}, {'foo': 'baz'}) resulta em um map contendo {'foo': 'baz'}.
Spreads com verificação de null (…?)
#
Um spread com verificação de null adiciona o conteúdo à coleção somente se a expressão depois do operador não for null:
List<String> combineIfExists(List<String> a, List<String> b) {
return [
...?a,
...?b,
];
}
void main() {
var result = combineIfExists(['foo'], null);
print(result); // [foo]
}
Collection if
#
Use as palavras-chave if, else e else if para adicionar algo a uma coleção com base em uma condição. Aqui está um exemplo de
collection if:
class Article {
String title;
DateTime date;
Article(this.title, this.date);
String toString() {
return [
if (title != null) title,
date.toString(),
].join(', ');
}
}
A palavra-chave else pode ser adicionada no fim:
String toString() {
return [
if (title != null) title else '(no title)',
].join(', ');
}
Repare onde está a vírgula. A vírgula não pode ficar depois de title, porque o else
faz parte do mesmo elemento. Manter o if e o else juntos antes da vírgula é o que os mantém separados do próximo elemento da coleção.
Adicionar um else if também funciona:
String toString() {
return [
if (title != null) title else '(no title)',
if (date == null)
'(no date)'
else if (date.year == DateTime.now().year)
'this year'
else
'${date.year}',
].join(', ');
}
Collection for
#Por fim, use a palavra-chave for para inserir uma sequência na coleção:
class Article {
String title;
DateTime date;
List<String> tags;
Article(this.title, this.date, this.tags);
String toString() {
return [
title,
date.toString(),
for (var tag in tags) 'tag: $tag'
].join(', ');
}
}
Neste exemplo, a expressão for adiciona uma string para cada item da lista tags. Assim como um
for normal em Dart, a expressão tags pode ser qualquer Iterable.
Coleções em código Flutter
#Se você usa Dart, há uma boa chance de estar usando-o para construir apps Flutter. Como os recursos descritos aqui foram projetados pensando no Flutter, vamos dar uma olhada em código Flutter.
Refatorando um método build()
#No Flutter, é comum montar uma lista de widgets em um método build():
[@override](http://twitter.com/override)
Widget build(BuildContext context) {
var articleWidgets = articles
.map<Widget>((article) => ArticleWidget(article: article))
.toList();
return ListView(
children: articleWidgets,
);
}
Isso poderia ser reescrito com um spread:
Widget build(BuildContext context) {
return ListView(
children: [
...articles.map((article) => ArticleWidget(article:article))
],
);
}
Ou com um collection for:
Widget build(BuildContext context) {
return ListView(
children: [
for (var article in articles)
ArticleWidget(article: article)
],
);
}
O primeiro trecho converte a lista de Article em uma coleção de objetos ArticleWidget
usando map() e, em seguida, aplica o operador spread para expandi-los na lista que os envolve. No segundo exemplo, o operador
collection for permite expressar isso de forma um pouco mais sucinta.
Um método build() maior
#Aqui está um exemplo mais complexo:
Widget build(BuildContext context) {
var headerStyle = Theme.of(context).textTheme.headline4;
return Column(
children: [
if (article.title != null)
Text(article.title, style: headerStyle),
if (article.date != null)
Text(article.date.toString()),
Text('Tags:'),
for (var tag in article.tags)
Text(tag),
],
);
}
A lógica para posicionar os widgets na Column fica exatamente onde o leitor esperaria, e economiza muito código. Antes desses recursos, a forma mais comum de obter o mesmo comportamento era criar uma variável e usar
if normais que chamavam add().
Combinando esses recursos
#Esses recursos podem ser combinados de formas interessantes, como mostram os exemplos desta seção. Aqui estão alguns pontos a ter em mente:
-
Sintaticamente, um collection if, um collection for ou um spread é um único elemento, mesmo que acabe criando vários objetos.
-
Qualquer expressão pode estar no corpo de um collection if ou de um collection for.
-
Qualquer elemento pode estar no corpo de um collection if ou de um collection for.
Usando if e for juntos
#Aqui está um código que cria uma lista usando um collection if dentro de um collection for. Aqui, cada artigo é adicionado à lista se for posterior a uma certa data:
List<Article> recentArticles(List<Article> allArticles) {
var ninetyDaysAgo = DateTime.now().subtract(Duration(days: 90));
return [
for (var article in allArticles)
if (article.date.isAfter(ninetyDaysAgo))
article
];
}
Se você preferir spreads, o valor de retorno poderia, em vez disso, ser escrito como ...allArticles.where((article) => article.date.isAfter(ninetyDaysAgo)).
Usando collection if e spreads juntos
#Um collection if recebe um único elemento, mas, se você quiser incluir mais de um, pode usar um spread:
Widget build(BuildContext context) {
return Column(
children: [
if (article.date != null) ...[
Icon(Icons.calendar_today),
Text('${article.date}'),
],
],
);
}
Usando recursos de coleção com async-await
#
Você também pode combinar chamadas assíncronas com literais de coleção. Por exemplo, um padrão comum é usar
Future.wait() para disparar um grupo de chamadas assíncronas:
Future<Article> fetchArticle(String id);
Future<List<Article>> fetchArticles() async {
return Future.wait([
fetchArticle('1'),
fetchArticle('2'),
fetchArticle('3'),
]);
}
Esse código pode ser melhorado usando um collection for:
Future<List<Article>> fetchArticles(List<String> ids) async {
return Future.wait([
for (var id in ids)
fetchArticle(id),
]);
}
Também é possível colocar um await em um literal de coleção, embora ele vá esperar cada future em sequência:
Future<List<Article>> fetchArticles(List<String> ids) async {
return [
// fetches one at a time
for (var id in ids)
await fetchArticle(id),
];
}
O código anterior espera em sequência porque é equivalente ao seguinte:
Future<List<Article>> fetchArticles() async {
return <Article>[
// fetches one at a time
await fetchArticle('1'),
await fetchArticle('2'),
await fetchArticle('3'),
];
}
Você também pode expandir um Stream usando await for:
Stream<String> get idStream => Stream.fromIterable(['1','2','3']);
Future<List<String>> gatherIds(Stream<String> ids) async {
return [
await for (var id in ids)
id
];
}
void main() async {
print(await gatherIds(idStream)); // [1, 2, 3]
}
Este é mais um exemplo de como collection if, collection for e spreads funcionam com outras partes da linguagem. Se você já usou uma instrução
await for, pode conseguir adivinhar o comportamento: ela escuta o stream em busca de novos valores e coloca o corpo na lista que a envolve.
Explorando mais
#Espero que estas dicas ajudem você a escrever um código Dart mais limpo. Há ainda mais formas de usar esses recursos do que as mencionadas aqui. Se você encontrar uma técnica legal, compartilhe com a comunidade ou mencione @dart_lang no Twitter. Para mais detalhes, confira Tornando o Dart uma linguagem melhor para UI ou a proposta inicial de linguagem no GitHub.
Mais do Dart
Trazendo construtores primários para o Dart
Um olhar por dentro das decisões de design, dos trade-offs e do açúcar sintático por trás da adição de construtores primários no Dart 3.13.
A história da interop com JS no Dart
O suporte ao Wasm acaba de chegar ao Flutter beta atual, graças a um marco empolgante de interop com JavaScript alcançado no Dart 3.3. Para celebrar…