top of page
Search
Writer's picturePavel Kalinin

New features in Dart 3.0



With release Dart 3 such new features how records, pattern matching and class modifiers. It will change a lot in how we write code and which libraries we use. Now I will tell you how the new features will help you in practice and how this will change the way you interact with the language.


Records



Traditionally, a Dart function could only return a single value. As a result, functions that needed to return multiple values had to either package these into other data types such as maps or lists or to define new classes that could hold the values. Using untyped data structures weakened type safety. Having to define new classes just to carry data adds friction during the coding process.

Before Dart 3.0, when I needed to return user location(latitude and longitude) from function. I created a new wrapper class location with latitude and longitude fields.


class Location {
  final double latitude;
  final double longitude;

  const Location({
    required this.latitude,
    required this.longitude,
  });
  ...
}

Location fetchUserLocation() {
   return Location(latitude: 53.0, longitude: 25.2);
}

But now, in Dart 3.0, I can replace class Location to record:


({double latitude, double longitude}) fetchUserLocation() {
   return (latitude: 53.0, longitude: 25.2);
}

For getting fields from new structure:


void main() {
  final ({double latitude, double longitude}) userLocation = fetchUserLocation();
  
  final double latitude = userLocation.latitude;
  final double longitude = userLocation.longitude;
}

But now I have duplicate code with record. I can fix it with using typedef. Finally result:


typedef Location =  ({double latitude, double longitude});

Location fetchUserLocation() {
   return (latitude: 53.0, longitude: 25.2);
}

void main() {
  final Location userLocation = fetchUserLocation();
  
  final double latitude = userLocation.latitude;
  final double longitude = userLocation.longitude;
}

But what is the records? Records are an anonymous, immutable, aggregate type. Like other collections types, they let you bundle multiple objects into a single object. Unlike other collection types, records are fixed-sized and typed. Record is super type of Never and subtype of Object and dynamic. Record type behaves in much the same way as a Function in the Dart type system.


Record equality

Two records are equal if they have the same shape (set of fields), and their corresponding fields have the same values. Since named field order is not part of a record’s shape, the order of named fields does not affect equality. Also, records automatically define hashCode and == methods based on the structure of their fields.

For example:


(int x, int y, int z) point = (4, 7, 1);
(int r, int g, int b) color = (4, 7, 1);

print(point == color); // Prints 'true'.
({int x, int y, int z}) point = (x: 4, y: 7, z: 1);
({int r, int g, int b}) color = (r: 4, g: 7, b: 1);

print(point == color); // Prints 'false'. Lint: Equals on         // unrelated types.

But with collections it not work, for example:


final values = [1, 2, 3];
(int x, int y, int z, List<int> data) point = (4, 7, 1, values);
(int r, int g, int b, List<int> data) color = (4, 7, 1, values);

print(point == color); // Prints 'true'.
(int x, int y, int z, List<int> data) point=(4, 7, 1, [1, 2, 3]);
(int r, int g, int b, List<int> data) color=(4, 7, 1, [1, 2, 3]);

print(point == color); // Prints ‘false’.

Records help work with data without creating wrapping classes. For my point of view, records will be helpfuI for transfer data between architecture layers. When you will be working with records, remember about especially work records with collections.


Class modifiers



Class modifiers control how a class or mixin can be used, both from within its own library, and from outside of the library where it’s defined.

Modifier keywords come before a class or mixin declaration. For example, writing abstract class defines an abstract class. The full set of modifiers that can appear before a class declaration include:

  • abstract

  • base

  • final

  • interface

  • sealed

  • mixin

Only the base modifier can appear before a mixin declaration. The modifiers do not apply to other declarations like enum, typedef, or extension.

You can combine some modifiers for layered restrictions. A class declaration can be, in order:

  1. (Optional) abstract, describing whether the class can contain abstract members and prevents instantiation.

  2. (Optional) One of base, interface, final or sealed, describing restrictions on other libraries subtyping the class.

  3. (Optional) mixin, describing whether the declaration can be mixed in.

  4. The class keyword itself.

The valid combinations of class modifiers and their resulting capabilities are:


Declaration

Construct

Extend

Implement

​Mixin

class

Yes

Yes

Yes

No

base class

Yes

Yes

No

No

interface class

Yes

No

Yes

No

final class

Yes

No

No

No

sealed class

No

No

No

No

abstract class

No

Yes

Yes

No

abstract base class

No

Yes

No

No

abstract interface class

No

No

Yes

No

abstract final class

No

No

No

No

mixin class

Yes

Yes

Yes

Yes

base mixin class

Yes

Yes

No

Yes

abstract mixin class

No

Yes

Yes

Yes

abstract base mixin class

​No

Yes

No

Yes

mixin

No

No

Yes

Yes

base mixin

No

No

No

Yes


You can’t combine some modifiers because they are contradictory, redundant, or otherwise mutually exclusive:

  • abstract with sealed. A sealed class is always implicitly abstract.

  • interface, final or sealed with mixin. These access modifiers prevent mixing in.

I think, class modifiers have redundant count of combination, but now they give more control over API for developers, who create packages. Also, you can use class modifiers in your project, but you must remember, they will be work outside declared file. For example:


// File animal.dart
abstract interface class Animal {}

final class Cat extends Animal {}
// File dog.dart
// Error: The class 'Animal' can't be extended outside of its     // library because it's an interface class.
final class Dog extends Animal {} 

Patterns and pattern matching



Dart has had limited support for switch since the beginning. Switch now support pattern matching in these cases. Now you don’t have to adding break at the end of each case. Switch also support logical operators to combine cases. The following example shows a nice and crisp switch statement that parses a character code:


switch (charCode) {
  case slash when nextCharCode == slash:
    skipComment();

  case slash || star || plus || minus:
    operator(charCode);

  case >= digit0 && <= digit9:
    number();

  default:
    invalid();
}

The switch statement provides a great help when you need one or more statements for each case. In some cases, all you want to do is to calculate a value. For that use case, you can use a very succinct switch expression. This resembles the switch statement, but uses different syntax that’s fine tuned for expressions. The following sample function returns the value of a switch expression to calculate a description of today’s weekday:


String describeDate(int weekday) => 
    switch (weekday) {
      >= 1 && <= 5 => 'Today is work day',
      6 || 7 => 'Today is weekend!',
      _ => 'Invalid day'
    };

A powerful feature of patterns is the ability to check for “exhaustiveness”, This feature ensures that the switch handles all the possible cases. In the previous example, we’re handling all possible values of weekday, which is an int. We exhaust all possible values through the combination of match statements for the specific values range from 1 to 56 or 7, and then using a default case _ for the remaining cases. To enable this check for user-defined data hierarchies, such as a class hierarchy, use the new sealed modifier on the top of the class hierarchy as in the following example:


sealed class Result {}
class Success extends Result {}
class Loading extends Result {}
class Failure extends Result {}

String handleResult(Result result) {
  return switch (result) {
    Success success => '$success',
    Loading loading => '$loading',
  };
}

This returns the following error, alerting us that we missed handling the last possible subtype, Failure:


line 7 • The type 'Result' is not exhaustively matched by the switch cases since it doesn't match 'Failure()'.

Finally, if statements can use patterns too. In the next example, we’re using if-case matching against a map-pattern to destructure the JSON map. Inside that, we’re matching against constant values (strings like 'name' and 'Pavel') and a type test pattern int age to read out a JSON value. If the pattern matches fail, Dart executes the else statement.


final json = {'name': 'Pavel', 'age': 26, 'height': 178};

// Find Pavel's age.
if (json case {'name': 'Pavel', 'age': int age}) {
  print('Pavel is $age years old.');
} else {
  print('Error: json contains no age info for Pavel!');
}

This just touches on all the things you can do with patterns.

To learn more, check out useful links:

https://dart.dev/language/records - records documentation

https://dart.dev/language/class-modifiers - class modifiers documentation

https://dart.dev/language/patterns - patterns and pattern matching documentation


Summary

In this article, I overviewed new language features that make Dart more expressive and crisp with records, patterns. For large API surfaces, class modifiers enable detailed control. Dart 2 was an extremely important and breaking update, but didn't change the way we use Dart that much. With Dart 3, it's the other way around: it won't force you to rewrite the entire project, but it will significantly change the way you work with it every day. Migration from Dart 2 to Dart 3 optional and you can migrate part by part, because Dart 3 add new and support old features.


162 views0 comments

Recent Posts

See All

Kommentare


bottom of page