Dart's Iterable
and List
offer a basic set of methods to modify and query collections. However, coming from the C# background, which has a marvelous LINQ-to-Objects library, I felt that a part of the functionality that I was using on a daily basis is missing. Of course, any task can be solved with the dart.core
methods only, but it sometimes requires multi-line solutions, and the code is not so obvious and takes effort to understand. We all know that developers spend a lot of time reading the code, and it is crucial to have it concise and obvious.
dartx package allows developers to use elegant and readable single-line operations on collections (and other Dart types).
Below we’ll compare how these tasks are solved with plain Dart vs. dartx:
- first / last collection item or null / default value / by condition;
- map / filter / iterate collection items depending on their index;
- converting a collection to a map;
- sorting collections;
- collecting unique items;
- min / max / average by items property;
- filtering out null items;
and look at some additional useful extensions.
All examples require dartx
dependency in pubspec.yaml
file and
import 'package:dartx/dartx.dart';
To get the first and last collection items in plain Dart one would write:
final first = list.first;
final last = list.last;
which throws a StateError
if list
is empty. Or explicitly return null
:
final firstOrNull = list.isNotEmpty ? list.first : null;
final lastOrNull = list.isNotEmpty ? list.last : null;
Using dartx allows:
final firstOrNull = list.firstOrNull;
final lastOrNull = list.lastOrNull;
Similarly:
final elementAtOrNull = list.elementAtOrNull(index);
returns null
if the index
is out of bounds of the list
.
Given you now remember that .first
and .last
getters throw errors when list
is empty, to get the first and last collections items or default value, in plain Dart you’d write:
final firstOrDefault = (list.isNotEmpty ? list.first : null) ?? defaultValue;
final lastOrDefault = (list.isNotEmpty ? list.last : null) ?? defaultValue;
Using dartx:
final firstOrDefault = list.firstOrDefault(defaultValue);
final lastOrDefault = list.lastOrElse(defaultValue);
Similar to elementAtOrNull
:
final elementAtOrDefault = list.elementAtOrDefault(index, defaultValue);
returns defaultValue
if the index
is out of bounds of the list
.
To get the first and last collection items that match some condition or null
, a plain Dart implementation would be:
final firstWhere = list.firstWhere((x) => x.matches(condition));
final lastWhere = list.lastWhere((x) => x.matches(condition));
which will throw StateError
for empty list unless orElse
is provided:
final firstWhereOrNull = list.firstWhere((x) => x.matches(condition), orElse: () => null);
final lastWhereOrNull = list.lastWhere((x) => x.matches(condition), orElse: () => null);
dartx shortens the implementation to:
final firstWhereOrNull = list.firstOrNullWhere((x) => x.matches(condition));
final lastWhereOrNull = list.lastOrNullWhere((x) => x.matches(condition));
It’s not a rare case when you need to obtain a new collection where each item somehow depends on its index. For example, each new item is a string representation of an item from the original collection + its index.
If you like me prefer one-liners, in plain Dart it would be:
final newList = list.asMap()
.map((index, x) => MapEntry(index, '$index $x'))
.values
.toList();
With dartx it’s much simpler:
final newList = list.mapIndexed((index, x) => '$index $x').toList();
I applied .toList()
because this and most other extension methods return lazy Iterable
.
For another example, let’s say there is a need to collect only odd-indexed items. With plain Dart, this can be implemented like:
final oddItems = [];
for (var i = 0; i < list.length; i++) {
if (i.isOdd) {
oddItems.add(list[i]);
}
}
Or in one line:
final oddItems = list.asMap()
.entries
.where((entry) => entry.key.isOdd)
.map((entry) => entry.value)
.toList();
With dartx it’s only:
final oddItems = list.whereIndexed((x, index) => index.isOdd).toList();
or:
final oddItems = list.whereNotIndexed((x, index) => index.isEven).toList();
How would you log collection content specifying item indexes?
In plain Dart:
for (var i = 0; i < list.length; i++) {
print('$i: ${list[i]}');
}
With dartx:
list.forEachIndexed((element, index) => print('$index: $element'));
For example, there is a need to convert a list of distinct Person
objects to a Map<String, Person>
where keys are person.id
, and values are whole person instances.
In plain Dart:
final peopleMap = people.asMap().map((index, person) => MapEntry(person.id, person));
With dartx:
final peopleMap = people.associate((person) => MapEntry(person.id, person));
or:
final peopleMap = people.associateBy((person) => person.id);
To get a map where keys are DateTime
and values are List<Person>
of people who were born that day, in plain Dart you’d write:
final peopleMapByBirthDate = people.fold<Map<DateTime, List<Person>>>(
<DateTime, List<Person>>{},
(map, person) {
if (!map.containsKey(person.birthDate)) {
map[person.birthDate] = <Person>[];
}
map[person.birthDate].add(person);
return map;
},
);
It’s much simpler with dartx:
final peopleMapByBirthDate = people.groupBy((person) => person.birthDate);
How would you sort a collection in plain Dart? You have to keep in mind that
list.sort();
modifies the source collection, and to get a new instance you’d have to write:
final orderedList = [...list]..sort();
dartx provides an extension to get a new List
instance:
final orderedList = list.sorted();
and:
final orderedDescendingList = list.sortedDescending();
And how would you sort collection items based on some property?
Plain Dart:
final orderedPeople = [...people]
..sort((person1, person2) => person1.birthDate.compareTo(person2.birthDate));
With dartx:
final orderedPeople = people.sortedBy((person) => person.birthDate);
and:
final orderedDescendingPeople = people.sortedByDescending((person) => person.birthDate);
To go even further, you can sort collection items by multiple properties:
final orderedPeopleByAgeAndName = people
.sortedBy((person) => person.birthDate)
.thenBy((person) => person.name);
and:
final orderedDescendingPeopleByAgeAndName = people
.sortedByDescending((person) => person.birthDate)
.thenByDescending((person) => person.name);
To get distinct collection items one might use this plain Dart implementation:
final unique = list.toSet().toList();
which does not guarantee to preserve items order or come up with a multi-line solution.
With dartx, it’s as easy as:
final unique = list.distinct().toList();
and:
final uniqueFirstNames = people.distinctBy((person) => person.firstName).toList();
To find a min / max collection item, we could for example sort it and take first
/ last
item:
final min = ([...list]..sort()).first;
final max = ([...list]..sort()).last;
The same approach can be applied to sort by items' property:
final minAge = (people.map((person) => person.age).toList()..sort()).first;
final maxAge = (people.map((person) => person.age).toList()..sort()).last;
or:
final youngestPerson =
([...people]..sort((person1, person2) => person1.age.compareTo(person2.age))).first;
final oldestPerson =
([...people]..sort((person1, person2) => person1.age.compareTo(person2.age))).last;
But with dartx it’s much easier:
final youngestPerson = people.minBy((person) => person.age);
final oldestPerson = people.maxBy((person) => person.age);
which by the way will return null
for empty collections.
And if collection items implement Comparable
, methods without selectors can be applied:
final min = list.min();
final max = list.max();
You can also easily obtain the average:
final average = list.average();
and:
final averageAge = people.averageBy((person) => person.age);
and the sum of num
collections:
final sum = list.sum();
or num
items' properties:
final totalChildrenCount = people.sumBy((person) => person.childrenCount);
With plain Dart:
final nonNullItems = list.where((x) => x != null).toList();
With dartx:
final nonNullItems = list.whereNotNull().toList();
There are other useful extensions in dartx. We won’t go into deeper details here, but I hope the naming and the code are self-explanatory.
final report = people.joinToString(
separator: '\n',
transform: (person) => '${person.firstName}_${person.lastName}',
prefix: '<<️',
postfix: '>>',
);
final allAreAdults = people.all((person) => person.age >= 18);
final allAreAdults = people.none((person) => person.age < 18);
final first = list.first;
final second = list.second;
final third = list.third;
final fourth = list.fourth;
final youngestPeople = people.sortedBy((person) => person.age).takeFirst(5);
final oldestPeople = people.sortedBy((person) => person.age).takeLast(5);
final orderedPeopleUnder50 = people
.sortedBy((person) => person.age)
.firstWhile((person) => person.age < 50)
.toList();
final orderedPeopleOver50 = people
.sortedBy((person) => person.age)
.lastWhile((person) => person.age >= 50)
.toList();
The dartx package contains many more extensions for Iterable
, List
, and other Dart types. The best way to explore its capabilities is by browsing the source code.
If you, as me, find the package useful, remember to give it 👍🏻 on pub.dev and ⭐️ on GitHub.
Thanks to package authors Simon Leier and Pascal Welsch.
Originally published on May 2021 under "Flutter community" Medium publication.
Featured in Google Dev Library under Flutter category.