Dart Record

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.$positionCode 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:

trueCode 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.0Code 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.
Was this tutorial helpful ?