Collection #
Introduction #
Almost all applications deals with collections of things in some way, making collections an important building block.
Lists #
A List
in Dart is similar to lists or arrays of other languages.
It is simply an ordered group of objects.
List<int> list = [1, 2, 3];
Notice: elements are encapsulated within square brackets.
If we leave out the type List<int>
then it will be inferred by the compiler
since all elements are int
.
var list = [1, 2, 3];
If we instead define the list as [1, 2, 3.0]
then it will be inferred as type
List<num>
, since num
is the closet common subclass of both int
and
double
.
There are two ways to define an empty list of some type.
List<int> list1 = [];
var list2 = <int>[];
Remember, if you leave out the type then it will be inferred as dynamic
.
void main() {
List<int> list1 = [1, 2, 3];
print("list1 is ${list1.runtimeType}");
var list2 = [1, 2, 3];
print("list2 is also ${list2.runtimeType}");
var list3 = [1, 2, 3.0];
print("list3 is ${list3.runtimeType} since all elements are num");
}
Set #
A set is an unordered collection of unique items.
Set<int> set = {1, 2, 3};
Notice: elements are encapsulated within curly-brackets.
Here is a silly example of what is meant by a collection unique items. If you do:
var set = {1, 2, 3, 3};
You will only get a set with 3 values because 3
and 3
are the same.
An example use case for sets are tags on a block post. This article could have the tags:
final tags = {"programming", "oop", "dart", "collections"};
We could find related posts by taking the intersection (overlap) between the two set of tags.
final otherTags = {"Python", "collections"};
final tagsInCommon = tags.intersection(otherTags);
The variable tagsInCommon
will be {"collections"}
since that is the only
element contained in both sets.
There are of cause many other use cases for sets. Just remember they don’t maintain order. Meaning you can’t rely on the order of elements when looping over a set being the same as they were added.
Maps #
A map is a collection of key/value pairs.
Map<String, dynamic> map = {
"name": "Joe",
"age": 21
};
Notice: syntax is similar to objects in JSON.
In the example above I’ve specified the value to be of type dynamic
.
If I had left out the type then it would be inferred as type Object
since
that is the common base class of both String
and int
.
You will work with maps when retrieving data from a web-API.
Iterables #
The collection types in Dart such as List
, Set
and Map
are Iterable
,
which provides you with a bunch of convenient methods for operating across
their elements.
These methods can be chained together making them really powerful.
Learning to utilize them allows you to express transformations more elegantly
than with explicit loops.
The concepts you will learn about in the section applies to many other mainstream programming languages as well, though semantics varies slightly.
Language comparison #
These kinds of methods exist in many programming languages, though naming might be different. So, there is a good chance that you can recognize having seen something similar in another language already.
Description | Dart | C# | JavaScript |
---|---|---|---|
Filter (keep) elements that match given predicate | where | Where | filter |
Map (convert) each element to another type | map | Select | map |
Flatten nested collections | expand | SelectMany | flatMap |
Group elements by a common value | groupBy | GroupBy | Object.groupBy |
Visualization #
Here are some examples to make it a bit easier to wrap your head around.
Input | Operation | Output |
---|---|---|
[🍔, 🍕, 🍔] | .where((x) => x == 🍔) | (🍔, 🍔) |
[🍔, 🍔, 🍔] | .map((x) => 🍕) | (🍕, 🍕, 🍕) |
[[🍕, 🍕], [🍔, 🍔]] | .expand((x) => x) | (🍕, 🍕, 🍔, 🍔) |
[[🍲, 🌶️], [🍲, 🍅], [🍞, 🧈]] | groupBy(list, (x) => x[0]) | {🍲: [[🍲, 🌶️], [🍲, 🍅]], 🍞: [[🍞, 🧈]]} |
Lists are represented with []
, maps in the form {"key": "value"}
and iterables with ()
.
Example usage #
Run the code an observe the result. You can play around with it if you want.
import "package:collection/collection.dart";
const movies = [
(title: "Alien", year: 1979),
(title: "Let the Right One In", year: 2008),
(title: "Aliens", year: 1986),
(title: "Jaws", year: 1975),
(title: "The Silence of the Lambs", year: 1991),
];
void main() {
print("\n[Newer than 1990]");
print(movies.where((m) => m.year > 1990));
print("\n[Decades movies where released in]");
print(movies.map((m) => "${m.year - (m.year % 10)}s"));
print('\n[Group by first letter of title]');
print(groupBy(movies, (m) => m.title[0]));
}
Exercise #
Implement each function so that the test pass.
Can you solve them without writing any loops?
Help:
Data #
const List<Person> people = [
(id: 1, name: "Guillaume Strasse", language: "Danish", age: 41),
(id: 2, name: "Anestassia Echallie", language: "English", age: 47),
(id: 3, name: "Laura Ringsell", language: "Swedish", age: 14),
(id: 4, name: "Huey Ragsdall", language: "Latvian", age: 78),
(id: 5, name: "Winny Pouton", language: "Danish", age: 72),
(id: 6, name: "Franzen Fahy", language: "Swedish", age: 86),
(id: 7, name: "Killie Spatoni", language: "English", age: 16),
(id: 8, name: "Damaris Grebner", language: "Swedish", age: 39),
(id: 9, name: "Haleigh Rheubottom", language: "Georgian", age: 99),
(id: 10, name: "Anabel Bariball", language: "English", age: 13),
(id: 11, name: "Lettie Toon", language: "Danish", age: 55),
(id: 12, name: "Ginger Alsopp", language: "Danish", age: 75),
(id: 13, name: "Lee Gazey", language: "English", age: 30),
(id: 14, name: "Timotheus Gosnall", language: "English", age: 82),
(id: 15, name: "Elsworth Huntly", language: "Korean", age: 9)
];
Age groups #
Age | Category |
---|---|
< 18 | adults |
< 18 | minors |
< 13 | kids |
> 13, < 18 | youngsters |
Code #
{$ begin main.dart $}
import "package:collection/collection.dart";
typedef Person = ({int id, String name, String language, int age});
bool anyKids(List<Person> people) {
// return `true` if anyone is under 13, otherwise `false`
return false;
}
bool anyYoungsters(List<Person> people) {
// return `true` if anyone is older than 13 but younger than 18
return false;
}
Person? firstYoungster(List<Person> people) {
// return the first person between 13 and 18
return null;
}
List<int> mapToIds(List<Person> people) {
// return a list of IDs for each person
return [];
}
int findIdByName(List<Person> people, {required String name}) {
// return the `id` of the first person matching the given `name`
return 0;
}
String? mostSpokenLanguage(List<Person> people) {
// return the most spoken language for all the people
return null;
}
List<String> top3MostSpokenLanguages(List<Person> people) {
// return the 3 most spoken languages
return [];
}
{$ end main.dart $}
{$ begin solution.dart $}
import "package:collection/collection.dart";
typedef Person = ({int id, String name, String language, int age});
bool anyKids(List<Person> people) {
return people.any((person) => person.age < 13);
}
bool anyYoungsters(List<Person> people) {
return people.any((person) => person.age > 13 && person.age < 18);
}
Person? firstYoungster(List<Person> people) {
try {
return people.firstWhere((person) => person.age > 13 && person.age < 18);
} on StateError {
return null;
}
}
List<int> mapToIds(List<Person> people) {
return people.map((e) => e.id).toList();
}
int findIdByName(List<Person> people, {required String name}) {
return people.where((e) => e.name == name).map((e) => e.id).single;
}
String? mostSpokenLanguage(List<Person> people) {
final group = people.groupFoldBy((element) => element.language,
(previous, element) => ((previous ?? 0) as num) + 1);
return maxBy(group.entries, (p0) => p0.value)?.key;
}
List<String> top3MostSpokenLanguages(List<Person> people) {
return people
.groupFoldBy(
(element) => element.language,
(previous, element) => ((previous ?? 0) as num) + 1,
)
.entries
.sortedBy((element) => element.value)
.reversed
.take(3)
.map((e) => e.key)
.toList();
}
{$ end solution.dart $}
{$ begin test.dart $}
// import "package:collection/collection.dart";
// import "./solution.dart";
// import "../mock_result.dart";
// const _result = result;
const List<Person> people = [
(id: 1, name: "Guillaume Strasse", language: "Danish", age: 41),
(id: 2, name: "Anestassia Echallie", language: "English", age: 47),
(id: 3, name: "Laura Ringsell", language: "Swedish", age: 14),
(id: 4, name: "Huey Ragsdall", language: "Latvian", age: 78),
(id: 5, name: "Winny Pouton", language: "Danish", age: 72),
(id: 6, name: "Franzen Fahy", language: "Swedish", age: 86),
(id: 7, name: "Killie Spatoni", language: "English", age: 16),
(id: 8, name: "Damaris Grebner", language: "Swedish", age: 39),
(id: 9, name: "Haleigh Rheubottom", language: "Georgian", age: 99),
(id: 10, name: "Anabel Bariball", language: "English", age: 13),
(id: 11, name: "Lettie Toon", language: "Danish", age: 55),
(id: 12, name: "Ginger Alsopp", language: "Danish", age: 75),
(id: 13, name: "Lee Gazey", language: "English", age: 30),
(id: 14, name: "Timotheus Gosnall", language: "English", age: 82),
(id: 15, name: "Elsworth Huntly", language: "Korean", age: 9)
];
class TestCase<T> {
final String name;
final T Function(List<Person> people) func;
final Equality<T> equality;
final T expected;
const TestCase(
this.name,
this.func,
this.equality,
this.expected,
);
}
final List<TestCase> testCases = [
TestCase("anyKids", anyKids, DefaultEquality(), true),
TestCase("anyYoungsters", anyYoungsters, DefaultEquality(), true),
TestCase("firstYoungster", firstYoungster, DefaultEquality(),
(id: 3, name: "Laura Ringsell", language: "Swedish", age: 14)),
TestCase("mapToIds", mapToIds, IterableEquality(),
List.generate(15, (index) => index + 1)),
TestCase(
"findIdByName",
(people) => findIdByName(people, name: "Lettie Toon"),
DefaultEquality(),
11),
TestCase(
"mostSpokenLanguage", mostSpokenLanguage, DefaultEquality(), "English"),
TestCase("top3MostSpokenLanguages", top3MostSpokenLanguages,
IterableEquality(), ["English", "Danish", "Swedish"])
];
void main() {
final results = testCases.map((e) {
final actual = e.func(people);
final success = e.equality.equals(actual, e.expected);
return (e, success: success, actual: actual);
});
results.forEach((e) {
if (e.success) {
print("\x1B[32m✅ ${e.$1.name}\x1B[0m");
} else {
print("\x1B[31m❌ ${e.$1.name}\x1B[0m");
print("Expected: ${e.$1.expected}");
print("Actual: ${e.actual}");
}
});
if (results.every((element) => element.success)) {
_result(true, ["Hurray, you did it 🥳"]);
} else {
_result(false, ["Not there yet!"]);
}
}
{$ end test.dart $}
{$ begin test_import.dart $}
part 'test.dart';
{$ end test_import.dart $}