Summary: in this tutorial, you will learn about the Dart Record type that allows you to bundle multiple objects into a single value.
Introduction to Dart record type
Dart 2 provides you with some options for bundling multiple objects into a single value.
First, you can create a class with fields for the values, which is suitable when you need meaningful behavior along with the data.
For example, you can create a Location
class that includes latitude and longitude:
class Location {
double lat;
double lon;
Location({
required this.lat,
required this.lon,
});
}
Code language: Dart (dart)
But this approach is verbose and may couple other codes to the specific class definition.
Alternatively, you can use different collection types like lists, maps, or sets. For example, the following uses a map to model a location that has two fields lat
and lon
:
final location = {'lat': 10.0, 'lon': 20.0};
Code language: Dart (dart)
The map is more lightweight than the class and avoids excessive coupling. However, it doesn’t work well with the static type system.
For example, if you need to bundle a number and a string, you can use a List<Object>
. But you will lose track of the number of elements and their individual types.
Dart 3 introduced the Record types to address these issues.
In Dart, records are anonymous, immutable, and aggregate type:
- Anonymous means that records don’t have a specific name associated with them. Unlike classes, which have a defined name, records are defined inline without a dedicated name. In practice, you often use records for one-time data structures or as a lightweight alternative to classes.
- Immutable means that records can’t be changed once they’re created. When you create a record, and set its fields, you cannot change them because those values remain constant throughout the lifetime of the record. The immutability ensures that records have a consistent state and promote safer and more practicable code.
- Aggregate type: records are aggregate types because they group multiple values into a single value. This allows you to treat the record as a cohesive unit, simplifying data manipulation and passing around the bundled data as a whole. For example, you can pass a record to or return it from a function.
Defining records
To define a record, you use a comma-delimited list of positional or named fields enclosed in parentheses.
For example, the following defines a record with two values latitude and longitude:
final location = (10.0, 20.0);
Code language: Dart (dart)
In this example, the location is a record with two values. The first value 10 represents the latitude and the second value 20 represents the longitude.
The fields of records may have a name to make it more clear. For example:
final location = (lat: 10.0, lon: 20.0);
Code language: Dart (dart)
In this example, we assign names lat and lon to the first and second fields.
To annotate the type of the record in a variable declaration, you use the following:
(double, double) location = (10.0, 20.0);
Code language: Dart (dart)
Also, you can name the positional fields in the record type annotation:
(double lat, double lon) location = (10.0, 20.0);
Code language: Dart (dart)
But these names are for documentation and they don’t affect the type of the record.
Accessing record fields
Since records are immutable, they have only getters. To access the positional field, you use the following syntax:
record.$position
Code language: Dart (dart)
For example:
void main() {
var location = (10.0, 20.0);
final lat = location.$1;
final lon = location.$2;
print('($lat, $lon)');
}
Code language: Dart (dart)
Output:
(10.0, 20.0)
Code language: Dart (dart)
In this example, we access the first and second fields using the location.$1
and location.$2
respectively.
If the fields have names, you can access the fields via their names directly like this:
void main() {
var location = (lat: 10.0, lon: 20.0);
final lat = location.lat;
final lon = location.lon;
print('($lat, $lon)');
}
Code language: Dart (dart)
Output:
(10.0, 20.0)
Code language: Dart (dart)
Record equality
Two records are equal when they meet two conditions:
- Have the same set of fields, a.k.a the same shape.
- Their corresponding fields have the same values.
Note that Dart automatically defines hashCode
and ==
methods based on the structure of the record’s fields.
For example:
void main() {
final loc1 = (10.0, 20.0);
final loc2 = (10.0, 20.0);
final result = loc1 == loc2;
print(result); // true
}
Code language: Dart (dart)
Output:
true
Code language: Dart (dart)
Dart record examples
Let’s take some practical examples of using the Dart records.
1) Returning multiple values from a function
The following example illustrates how to use a record to return the min and max of a list of numbers from a function:
void main() {
final result = minmax([5, 2, 3, 7, 0, -1]);
print(result);
}
(double?, double?) minmax(List<double> numbers) {
if (numbers.length == 0) {
return (null, null);
}
double min = numbers[0];
double max = numbers[0];
for (int i = 1; i < numbers.length; i++) {
if (numbers[i] < min) {
min = numbers[i];
}
if (numbers[i] > max) {
max = numbers[i];
}
}
return (min, max);
}
Code language: Dart (dart)
How it works.
First, define the minmax()
function that accepts a list of numbers and returns a record that has two fields, the first field represents the min and the second field represents the max.
Second, if the list is empty, the minmax()
function returns a record that contains two null values. Otherwise, it returns the corresponding min and max of the number.
Instead of retrieving record values from a return, you can restructure the values into local variables:
void main() {
final (min, max) = minmax([5, 2, 3, 7, 0, -1]);
print('min: $min, max: $max');
}
Code language: Dart (dart)
Output:
min: -1.0, max: 7.0
Code language: Dart (dart)
To annotate the name to record fields of the return type of a function, you use the {} as follows:
void main() {
final result = minmax([5, 2, 3, 7, 0, -1]);
print(result.min); // -1.0
print(result.max); // -7.0
}
({double? min, double? max}) minmax(List<double> numbers) {
if (numbers.length == 0) {
return (min: null, max: null);
}
double min = numbers[0];
double max = numbers[0];
for (int i = 1; i < numbers.length; i++) {
if (numbers[i] < min) {
min = numbers[i];
}
if (numbers[i] > max) {
max = numbers[i];
}
}
return (min: min, max: max);
}
Code language: Dart (dart)
2) Using records with future
When you make a call to an API via HTTP request, there are two cases:
- Success
- Failed
Typically, you need to throw an exception in case the API call fails. But with the record, you can return both the result as well as the message indicating the status.
We’ll make an API call to the endpoint https://jsonplaceholder.typicode.com/todos/1
that gets a todo by an id.
First, define the Todo model (todo.dart
)
import 'dart:convert';
class Todo {
int id;
String title;
bool completed;
Todo({
required this.id,
required this.title,
required this.completed,
});
factory Todo.fromMap(Map<String, dynamic> map) => Todo(
id: map['id'] as int,
title: map['title'] as String,
completed: map['completed'] as bool,
);
factory Todo.fromJson(String source) =>
Todo.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'Todo(id:$id, title: $title, completed: $completed)';
}
Code language: JavaScript (javascript)
Second, define a function that calls the API in the todo_services.dart
:
import 'package:http/http.dart' as http;
import 'todo.dart';
Future<(Todo?, String)> fetchTodo(int id) async {
final uri = Uri(
scheme: 'https',
host: 'jsonplaceholder.typicode.com',
path: 'todos/$id',
);
try {
var response = await http.Client().get(uri);
// if not OK
if (response.statusCode != 200) {
return (
null,
'Failed to fetch todo: ${response.statusCode}, ${response.reasonPhrase}'
);
}
final todo = Todo.fromJson(response.body);
return (todo, 'Success');
} catch (e) {
return (null, 'Error to fetch todo: $e');
}
}
Code language: JavaScript (javascript)
The fetchTodo()
returns a Future
with the record type (Todo?, String)
.
If the todo is not null, it means that the request succeeds with the successful message stored in the second field of the record. Otherwise, the message will store the error message.
Third, fetch the todo with id 1 and using the fetchTodo()
function:
import 'todo_services.dart';
void main() async {
var (todo, message) = await fetchTodo(1);
todo != null ? print(todo) : print(message);
}
Code language: JavaScript (javascript)
Finally, fetch a todo that doesn’t exist.
import 'todo_services.dart';
void main() async {
var (todo, message) = await fetchTodo(10000);
todo != null ? print(todo) : print(message);
}
Code language: JavaScript (javascript)
Output:
Failed to fetch todo: 404, Not Found
Summary
- Dart record is an anonymous, immutable, and aggregate type
- Use Dart record to model a lightweight data structure that contains multiple fields.