Explorando colecciones en Dart
Si alguna vez llamaste add(), addAll(), map() o toList() para construir una lista o mapa, quizás quieras revisar collection if, collection…
Si alguna vez has llamado add(), addAll(), map(), o toList()
para construir una lista o un map, quizás quieras revisar collection if, collection for, y
spreads. El año pasado, Dart añadió estas características en la versión 2.3.
En este artículo, veremos colecciones, exploraremos estas nuevas características y veremos algunos ejemplos interesantes. Al dominar estas características, puedes hacer tu código más conciso y más fácil de leer.
Colecciones
#Primero, necesitamos entender qué es una colección. Una colección es un objeto que contiene otros objetos. Por ejemplo:
-
List: una colección ordenada de objetos con una longitud (también llamada array)
Set: una colección desordenada de objetos únicos
Map: una colección desordenada de pares clave-valor
Queue: una colección ordenada que puede añadir/eliminar objetos por ambos extremos
-
SplayTreeMap: una colección ordenada de pares clave-valor basada en un árbol binario auto-balanceado
Estos tipos están disponibles en el paquete dart:collection. Para aún más tipos de colecciones, revisa package:collection en pub.dev.
Cada uno de estos tipos de colección implementa Iterable, lo que proporciona comportamiento común como ejecutar una función sobre cada objeto de la colección, obtener el primer objeto, determinar la longitud de la colección, y más.
Literales de colección
#Dart soporta sintaxis para construir tres tipos de colecciones: literales de lista ([]), literales de map ({}) y literales de set (también
{}).
Aquí hay un literal de lista:
List<String> getArtists() {
return [
'Picasso',
'Warhol',
'Monet',
];
}
Aquí hay un literal de map:
Map<String, String> getArtistsByPainting {
return {
'The Old Guitarist': 'Picasso',
'Orange Prince': 'Warhol',
'The Water Lily Pond': 'Monet',
};
}
Y aquí hay un literal de set, añadido en Dart 2.3:
Set<String> getArtistsSet() {
return {
'Picasso',
'Warhol',
'Monet',
};
}
Si te preguntas por qué los maps y los sets pueden usar la misma sintaxis {}, es porque Dart usa
inferencia de tipos para diferenciarlos. El sistema de tipos determina el tipo basándose en el tipo de los parámetros
a y b. Normalmente puede determinarlo según el contenido—por ejemplo, {1}
es obviamente un Set, y {1: 2} es obviamente un Map.
Nota: Usar
{}construye un map por defecto. Para crear un set, puedes usar una anotación de tipo genérico:<String>{}. Usar dos parámetros de tipo genérico crea un map:<String, String>{}.
Tipos de elementos
#Cada elemento en un literal de colección suele ser un valor o una expresión, pero también puede ser una de estas nuevas características: un collection if, collection for, o spread. Todos estos se llaman elementos.
Cada elemento desempaqueta cero o más items y los coloca en la colección circundante. Por ejemplo, un literal de string (digamos,
"oatmeal") resulta en un item, pero un collection for desempaqueta 0 o más items. Estas características también se pueden combinar de formas interesantes, como exploraremos a continuación.
Spreads
#Un spread toma una colección (por ejemplo, una lista) y coloca sus contenidos en la colección circundante:
List<String> combineLists(List<String> a, List<String> b) {
return [
...a,
...b,
];
}
El código anterior es equivalente a este:
List<String> combineLists(List<String> a, List<String> b) {
var list = [];
list.addAll(a);
list.addAll(b);
return list;
}
También puedes usar spreads en literales de map y 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 en maps como en sets, los contenidos de b sobrescriben los contenidos de a cuando hay un conflicto. Por ejemplo, llamar a
combineMaps({'foo': 'bar'}, {'foo': 'baz'}) da como resultado un map que contiene {'foo': 'baz'}.
Spreads con manejo de null (…?)
#
Un null-aware spread añade los contenidos a la colección solo si la expresión después del operador no es 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
#Usa las palabras clave if, else, y else if para añadir algo a una colección basándose en una condición. Aquí hay un ejemplo de uso de
collection if:
class Article {
String title;
DateTime date;
Article(this.title, this.date);
String toString() {
return [
if (title != null) title,
date.toString(),
].join(', ');
}
}
La palabra clave else se puede añadir al final:
String toString() {
return [
if (title != null) title else '(no title)',
].join(', ');
}
Nota dónde está la coma. La coma no puede estar después de title porque el else es parte del mismo elemento. Mantener el
if y el else juntos antes de la coma es lo que los mantiene separados del siguiente elemento de la colección.
Añadir un else if también 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 último, usa la palabra clave for para insertar una secuencia en la colección:
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(', ');
}
}
En este ejemplo, la expresión for añade un string por cada item en la lista tags.
Al igual que un bucle for normal en Dart, la expresión tags puede ser cualquier
Iterable.
Colecciones en código de Flutter
#Si estás usando Dart, hay una buena posibilidad de que lo estés usando para construir apps de Flutter. Dado que las características descritas aquí fueron diseñadas pensando en Flutter, echemos un vistazo a algo de código de Flutter.
Refactorizando un método build()
#En Flutter, es común construir una lista de widgets en un 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,
);
}
Esto se podría reescribir con un spread:
Widget build(BuildContext context) {
return ListView(
children: [
...articles.map((article) => ArticleWidget(article:article))
],
);
}
O con un collection for:
Widget build(BuildContext context) {
return ListView(
children: [
for (var article in articles)
ArticleWidget(article: article)
],
);
}
El primer fragmento convierte la clase Article en una colección de objetos ArticleWidget
usando map(), y luego aplica el operador spread para expandirlos en la lista circundante. En el segundo ejemplo, el operador
collection for te permite expresarlo de forma un poco más concisa.
Un método build() más grande
#Aquí hay un ejemplo más complejo:
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),
],
);
}
La lógica para colocar widgets en el Column está justo donde un lector podría esperar, y ahorra mucho código. Antes de estas características, la forma más común de lograr el mismo comportamiento era crear una variable y usar sentencias
if normales que llamen a add().
Combinando estas características
#Estas características se pueden combinar de formas interesantes, como muestran los ejemplos en esta sección. Aquí hay algunas cosas a tener en cuenta:
-
Sintácticamente, un collection if, collection for, o spread es un único elemento — incluso si termina creando múltiples objetos.
Cualquier expresión puede ir en el cuerpo de un collection if o collection for.
-
Cualquier elemento puede ir en el cuerpo de un collection if o collection for.
Usar if y for juntos
#Aquí hay algo de código que crea una lista usando un collection if dentro de un collection for. Aquí, cada artículo se añade a la lista si es posterior a una cierta fecha:
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
];
}
Si prefieres spreads, el valor de retorno podría escribirse en su lugar como ...allArticles.where((article) => article.date.isAfter(ninetyDaysAgo)).
Usar collection if y spreads juntos
#Un collection if toma un único elemento, pero si quieres incluir más de uno puedes usar un spread:
Widget build(BuildContext context) {
return Column(
children: [
if (article.date != null) ...[
Icon(Icons.calendar_today),
Text('${article.date}'),
],
],
);
}
Usar características de colección con async-await
#También puedes combinar llamadas asíncronas con literales de colección. Por ejemplo, un patrón común es usar
Future.wait() para disparar un grupo de llamadas asíncronas:
Future<Article> fetchArticle(String id);
Future<List<Article>> fetchArticles() async {
return Future.wait([
fetchArticle('1'),
fetchArticle('2'),
fetchArticle('3'),
]);
}
Ese código se puede mejorar usando un collection for:
Future<List<Article>> fetchArticles(List<String> ids) async {
return Future.wait([
for (var id in ids)
fetchArticle(id),
]);
}
También es posible poner un await en un literal de colección, aunque esperará por cada future en secuencia:
Future<List<Article>> fetchArticles(List<String> ids) async {
return [
// fetches one at a time
for (var id in ids)
await fetchArticle(id),
];
}
El código anterior espera en secuencia porque es equivalente al siguiente:
Future<List<Article>> fetchArticles() async {
return <Article>[
// fetches one at a time
await fetchArticle('1'),
await fetchArticle('2'),
await fetchArticle('3'),
];
}
También puedes expandir un 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 es otro ejemplo de cómo collection if, collection for, y spreads funcionan con otras partes del lenguaje. Si has usado una sentencia
await for, quizás puedas adivinar el comportamiento: escucha el stream en busca de nuevos valores y coloca el cuerpo en la lista circundante.
Explorando más allá
#Esperamos que estos consejos te ayuden a escribir código Dart más limpio. Hay aún más formas de usar estas características de las que se mencionan aquí. Si encuentras una buena técnica, compártela con la comunidad o menciona a @dart_lang en Twitter. Para más detalles, revisa Making Dart a Better Language for UI o la propuesta inicial del lenguaje en GitHub.
Más de Dart
Historia del interop con JS en Dart
El soporte para Wasm acaba de llegar al beta actual de Flutter, gracias a un emocionante hito de interop con JavaScript alcanzado en Dart 3.3. Para…
Dart DevTools: Analizando el rendimiento de la aplicación con el CPU Profiler
Ya seas un desarrollador de backend escribiendo una herramienta de línea de comandos con Dart, o un ingeniero de UX construyendo aplicaciones con Flutter, el rendimiento del programa es crítico para el éxito de tu proyecto. Las herramientas de línea de comandos deberían minimizar la latencia y las apps deberían ser responsivas y ágiles sin fotogramas perdidos. Como desarrolladores, hacemos todo lo posible para escribir código performante pero a veces no está claro por qué nuestro código no funciona tan bien como esperaríamos.