Uses of Dart records

This is an offshoot of the Lesser known classes and functions from the Dart core libraries discussion where @dominik and @BlueAquilae started discussing records.

Dominik kicked it off with:

Robert added:

Then @escamoteur pointed out this deserves a separate topic thread, which I’m now starting.

My addition is destructuring classes. It’s an easy way to get fields:

class Spaceship {
  String name;
  int year;
  String country;
  
  Spaceship(this.name, this.year, this.country);
}

Spaceship getSpaceship() {
  return Spaceship("Voyager 1", 1977, "USA");
}

void main() {
  var Spaceship(:name, :country) = getSpaceship();
  
  print('We got $name ($country).');
}

The above code is as if we got the spaceship object first (var s = getSpaceship()) and then extracted the fields (var name = s.name; var country = s.country;). Except by using records, we’re not polluting our local namespace with s.

To be honest, I haven’t been using this class restructuring syntax in my code so far. But maybe it’s because I tend to forget that the feature is there.

What other cool uses do you have for records? Or maybe there are uses of records that you learned to avoid?

14 Likes

I would be remiss not to include the link to official docs:

4 Likes

Destructuring classes looks so unfamiliar! For me the fact that you wrap a record with Spaceship() as if this was a constructor suggest that a new Spaceship class gets created. It’s definitely cool feature, but quite confusing if you don’t know the syntax.

4 Likes

One use case where I have found this class destructuring to be quite useful is something like this

List<DateTime> dates = [
  DateTime(2000, 10, 2),
  DateTime(2000, 10, 3),
];

for (final DateTime(:day, :month) in dates) {
  print('$day $month');
}
9 Likes

I too find the syntax hightly confusing. This is how f# does it

Type safe API call with Future.wait() using FutureRecord Extension upto 9 futures.

6 Likes

Using records with switch statements when evaluating multiple conditions:

void main() {
  print(getMessage(canPrint: true, canEdit: true));
  print(getMessage(canPrint: true, canEdit: false));
  print(getMessage(canPrint: false, canEdit: true));
  print(getMessage(canPrint: false, canEdit: false));
}

String getMessage({required bool canPrint, required bool canEdit}) {
  return switch ((canPrint, canEdit)) {
        (true, true) => 'You can print and edit',
        (true, false) => 'You can print, but cannot edit',
        (false, true) => 'You cannot print, but can edit',
        (false, false) => 'You cannot print nor edit',
      } +
      ' the document.';
}

Output:

You can print and edit the document.
You can print, but cannot edit the document.
You cannot print, but can edit the document.
You cannot print nor edit the document.
10 Likes

Suggestion adopted as a very good practice!

Negative points are:

  • Readability
  • Discovery
  • Renaming

Nothing in the IDE tells you what you’re working with, not the buggy insertion of type that disappear nor the hover than can not link to declaration.
It also mean no source navigation.

The named Records can not be renamed, you’re stuck with manual or global replace if possible

I used them with parsimony and prototyping but often and quickly I move to classes.
I’ve badly used them as configurator objects or function returns but have replaced when consolidating.

5 Likes

I often often use them now to pass more than one parameter to a flutter_command

1 Like

I just discovered this and was amazed. This simplifies a lot.

I mostly use Dart records to return multiple objects from functions. I wrote a blog post about it. I like that you can use named fields so that you are always aware of what the function is returning.

4 Likes

I like to pair them with typedef it helps with re-usability :slight_smile:

typedef MyRecord = (string first, int second)

MyRecord function() { ... }
void function(MyRecord record) { ... }
1 Like

Just keep in mind that if I define typedef RandalRecord = (String first, int second); they are completely interchangeable, unlike when you define proper Data classes, similar to how typedef X = String; doesn’t create a new type of string… it just aliases them together.

1 Like