Dart 3.0: brief overview

Dart 3.0: brief overview
Photo by Miikka Luotio / Unsplash

Heads up folks! Only three days ago at Google I/O, the new Dart 3.0 was unveiled! Like a piñata bursting open, it's spilling with a bunch of fun features: records, pattern-matching, and class modifiers. So, grab your biscuits, pour a hot cuppa, and let's dive into this tech fiesta! 🚀


Records

Ever wished your function could return more than one value, like a generous granny handing out sweets? Well, Dart 3.0's got you covered!

/// function that returns a tuple of two strings
(String, String) getToken() => ('access', 'refresh');

void main(List<String> args) {
  // create two variables and assign them to the values
  // returned by the function
  final (accessToken, refreshToken) = getToken();
}

Record with positional params

Voila! We just made a function return two values. But there's more! Dart 3.0's records have a softer side; they support named parameters. Let's spice up our codebase:

/// function that returns a tuple of two strings
({String access, String refresh}) getToken() => (access: 'access', refresh: 'refresh');

void main(List<String> args) {
  // create two variables and assign them 
  // to the values returned by the function
  var (:access, :refresh) = getToken();
  print(access);
  print(refresh);
}

Record with named params

Records quick tip

Neat, isn't it? But what if your record has more fields than a corn farm? Don't worry, Dart has got a neat trick up its sleeve:

// define typedef for a function
typedef TokenPair = ({String access, String refresh});

// use typedef as a return value
TokenPair getToken() => (access: 'access', refresh: 'refresh');

void main(List<String> args) {
  // create two variables and assign them to the values returned by the function
  var (:access, :refresh) = getToken();
  print(access);
  print(refresh);
}

Records with typedef


Destructuring

Speaking of magic tricks, Dart 3.0 also introduces destructuring. This nifty feature lets you unpack values from arrays or properties from objects into distinct variables, like a magician pulling rabbits out of a hat!

void main(List<String> args) {
  final list = ['Hello1', 'Hello2'];
  final [hello1, hello2] = list;
  print(hello1); // prints 'Hello1'
  print(hello2); // prints 'Hello2'
}

Applied destructuring to list

In this magic trick, I created two variables with list destructuring. But the fun doesn't stop there. Did you know you can use destructuring with Dart classes? Brace yourself!

void main(List<String> args) {
  final person = Person('John', 30, 'New York');
  final Person(:name, :age, :address) = person;
  print(name); // Prints John
  print(age); // Prints 30
  print(address); // Prints New York
}

class Person {
  final String name;
  final int age;
  final String address;

  Person(this.name, this.age, this.address);
}

Applied destructuring to a class Person

Plus, with destructuring, swapping variable values is as easy as swapping socks:

void main(List<String> args) {
  var a = 1;
  var b = 2;

  [a, b] = [b, a];
  print(a); // Prints 2
  print(b); // Prints 1
}

Now we're cooking with gas! But wait, there's more. Let's take a look at class modifiers.


Class Modifiers

Now we are moving to classes. Finally, they got modifiers! Let's list them:

  1. interface class - Can't be extended, only implemented. Like a stubborn mule, it refuses to change its ways.
  2. base class: Can't be implemented, only extended. It's like a family heirloom, passing on its legacy.
  3. final class: Can't be extended or implemented. Think of it as the lone wolf of classes.
  4. mixin class: Can be mixed in. It's the social butterfly in the world of classes.
  5. sealed class: Provides exhaustive check. The detective of classes, leaving no stone unturned.
👀
These rules apply only within a library. In the world of Dart, a library equals a file.
Table Of Definitions
// file_a.dart
interface class A {}

// but
class C extends A {}

// file_b.dart
// you cannot extend interface, only implement it
class B extends A {}

Interface Class

// file_a.dart
base class A {}

// file_b.dart
// class A can be only extended, not implemented
// moreover, the successor should be base class, too
base class B extends A {}

Base Class

// file_a.dart
final class A {}

// file_b.dart
// Class A cannot be nor extended nor implemented
class B extends A {}

Final Class

// file_a.dart
mixin class A {}

// file_b.dart
// Class A can be mixed in
class B with A {}

Mixin Class

// file_a.dart
sealed class A {}

class B extends A {}

class C extends A {}

// file_b.dart
// Class A can be nor extended nor implemented
// like final class, but it provides an exhaustive check
class D extends A {}

Sealed Class

It seems we are bending some of these rules, right? That's because these restrictions only apply outside of the library (file). And now, we have the cherry on top - Sealed Classes and Pattern Matching!


Sealed Classes & Pattern Matching

Exhaustive Type Checking is like the ultimate spell check but for your code. It ensures that you've covered all possible scenarios and left no possibility unchecked. Actually, this was already implemented to enums, but Dart 3.0 brings this to sealed classes.

final hello = Hello.hello;
switch (hello) {
  case Hello.hello:
    print('Hello');
    break;
  case Hello.world:
    print('World');
    break;
}

Enum exhaustiveness check

Sealed Classes

And now, let's see this in action with sealed classes:

sealed class A {}

class B extends A {}

class C extends A {}

void main(List<String> args) {
  final b = B();
  
  final isC = switch (b) {
    C() => true,
    _ => false,
  };

  print(isC); // prints false
}

Sealed Classes & Exhaustive Check

So, what's the lowdown on this code? We've birthed a variable into existence and we're playing a little game of 'Guess Who'. If our variable 'b' turns out to be the mysterious 'C', we reward ourselves by setting 'isC' to true. If not, alas, 'isC' gets a thumbs down with false. But wait, there's more! Let's crank this up a notch, shall we?

sealed class A {}

class B extends A {}

class C extends A {
  C(this.name);
  final String name;
}

void main(List<String> args) {
  final A b = C("Hello");
  
  // you must include all successors or default case
  final isC = switch (b) {
    C c => c.name,
    _ => "",
  };

  print(isC); // prints "Hello"
}

I reckon you've already got a taste of how this feature can be as potent as a shot of espresso.

Pattern Matching: Examples

I've cooked up a delightful smorgasbord of pattern matching examples for your coding palate. Feast your eyes on these!

void main(List<String> args) {
  final json = {
    'name': 'John',
    'age': 30,
    'address': 'New York',
  };

  if (json case {'name': 'Michael'}) {
    print('The username is Michael!!!');
  } else if (json case {'name': String name}) {
    print('The username is $name'); // prints The username is John
  }
}

New case syntax

void main(List<String> args) {
  final number = 10;
  final someEnv = false;

  final result = switch (number) {
    > 100 && < 1000 => 'Between 100 and 1000',
    > 100 when number % 2 == 0 => 'Greater than 100 and even',
    > 100 when someEnv => 'Greater than 100 and someEnv',
    < 10 => 'Less than 10',
    > 10 => 'Greater than 10',
    _ => 'Number $number is equal to 10',
  };

  print(result); // prints Number 10 is equal to 10
}

Pattern Matching #1

void main(List<String> args) {
  final diverseList = [1, 2, 'Hello', 'World', true, false];

  for (var item in diverseList) {
    if (item case int n) {
      print('Integer: $n'); // prints only integers
    }
  }
}

Pattern Matching #2

void main(List<String> args) {
  final json = {
    'name': 'John',
    'age': 30,
    'address': 'New York',
  };

  final name = switch (json) {
    {'name': var n} => n,
    _ when json.isEmpty => throw Exception('Empty JSON'),
    _ => throw Exception('No name found'),
  };
  print(name); // prints John
}

Pattern Matching #3

void main(List<String> args) {
  final record = ("John", 30, "New York");

  final ageIsHigher = switch (record) {
    (String _, int age) when age > 30 => true,
    _ => false,
  };

  print(ageIsHigher); // Prints false
}

Pattern Matching #4

As you can see, the shiny new pattern matching system is like our coding superhero, bestowing upon us the power to create super-efficient exhaustive checks. This kind of approach is like a handy multi-tool, ready to be whipped out for a variety of code conundrums.


Conclusion

Well, we've had a rollicking ride with Dart 3.0! It's been exciting, it's been fun, and there's more coming! So stay tuned for more updates, and remember, in the world of coding, always keep your sense of humor!

Read more