Sometimes we run into a case when a function logically needs to return multiple values. Some examples are returning coordinates of a point, returning some statistical data, returning error information like the error code and the message, etc.
The traditional method of doing this in Dart is to return the multiple values embedded into a List object.
void main() {
final json = {
'anime': 'Erased',
'episodes': 12,
};
final info = animeInfo(json);
final anime = info[0] as String;
final episodes = info[1] as int;
}
List<Object> animeInfo(Map<String, dynamic> json) => [
json['anime'] as String,
json['episodes'] as int,
];
Limitation:
The drawback of the above approach is that we have to explicitly downcast each element. The reason is that List assumes that all the elements have the same type (Object type in our example). The type system lost track of the fact that anime
is a String
and episodes
is an int
.
Another approach to solve this is to define a class that holds both these properties.
void main() {
final json = {
'anime': 'Erased',
'episodes': 12,
};
final info = animeInfo(json);
final anime = info.anime;
final episodes = info.episodes;
}
AnimeInfo animeInfo(Map<String, dynamic> json) => AnimeInfo(
anime: json['anime'] as String,
episodes: json['episodes'] as String,
);
class AnimeInfo {
final String anime;
final int episodes;
Anime({
required this.anime,
required this.episodes,
});
}
The above approach is type-safe which is nice and all the casts are gone. But it's verbose to have to define and name some class that just bundles up a couple of bits of data.
Note: Here we're not talking about DTOs and model data classes. Rather we're addressing the use cases where in your business logic you want to bundle tiny bits of some related data.
Dart 3.0 approach: Records
At the Flutter Forward event, Google shared its plans for the next major release of Dart, version 3.0, set to release later this year.
Dart 3.0 introduces a new built-in collection type - Records
A Record expression looks like a List literal but with parentheses instead of square brackets.
Let's see how it looks in the code.
void main() {
final json = {
'anime': 'Erased',
'episodes': 12,
};
final info = animeInfo(json);
final anime = info.$1;
final episodes = info.$2;
}
(String, int) animeInfo(Map<String, dynamic> json) => (
json['anime'] as String,
json['episodes'] as int,
);
A Record's type-annotation is also a parenthesized list of fields with each field having its type mentioned explicitly.
On the caller side, we just have getters whose name starts with the $ sign.
We couldn't use the subscript operator with an index because again each field can have a different, and the subscript operator wouldn't be able to infer the type of the element.
What are Records semantically?
The Record collection type is first class which simply means that it can be stored in a variable, can be passed as a function argument, can be returned from a function, and can be used as a generic type parameter.
Records are value types. They automatically define
==
andhashcode
in terms of their fields. Two records are equal if they have the same set of fields and their fields are equal.
Destructuring
Records are pretty nice, but the expression syntax for accessing fields is still pretty verbose.
final info = animeInfo(json);
final anime = info.$1;
final episodes = info.$2;
We can solve this by destructuring using something called a pattern.
final (anime, episodes) = animeInfo(json);
We can make use of a _
wildcard if we want to discard a field.
final (anime, _) = animeInfo(json)
We can also use existing variables if we want to change their values.
void main() {
var anime = 'Death Note';
var episodes = 37;
if (udpateAnime) {
(anime, episodes) = animeInfo(json);
}
}
So that's the basics of Records and Destructuring.
In the next article in this series, we'll talk about some more aspects of destructuring in Dart 3.0 as well as a very important application of Records in the Flutter realm.