New features in Dart 3.0

Kanan YusubovKanan Yusubov
7 min read

Patterns

According to documentation:

In general, a pattern may match a value, destructure a value, or both, depending on the context and shape of the pattern.

What can we do with Patterns? Let's deep dive into the examples.

We can get elements of the list:

void main() {
 final list = [10, 20, 30, 40, 50];
 final [a, b, c, d, e]  = list;
  print('$a $b $c $d $e'); // it will print 10 20 30 40 50
}

We can write more complex patterns and destructuring cases in switch:

/// this example firstly checks if list first element is a or b
/// then it destructs second element of list
switch (list) {
  case ['a' || 'b', var c]:
    print(c);
}

void main() {
  check(1); // it is equal to 1
  check(7); // it is between 5 and 10
  check(20); // it is 20
}


void check(int a) {
  switch(a) {
    case 1:
      print('it is equal to 1');
      break;
    case >= 5 && <= 10:
      print('it is between 5 and 10');
      break;
    default:
      print('it is $a');
  }
}

We can validate json in a more readable way:

void main() {
 final json = <String, dynamic>{
   'name': 'Kanan',
   'age': 24,
 };

  final (name, age) = getNameAndAge(json);
  print('$name: $age');
}

(String, int) getNameAndAge(Map<String, dynamic> json) {
  if(json case {'name': 'Kanan', 'age': 24}) {
     return (json['name'], json['age']);
  }

  throw Exception('it is not valid!');
}

We can use patterns in for and for-in loops:

void main() {
  final personList = <Person>[
    Person(name: 'Kanan', age: 24),
    Person(name: 'Zahra', age: 23)
  ];

  for(var Person(name: name, age: age) in personList) {
    print('name: $name, age: $age');
  }

  /// it will print
  // name: Kanan, age: 24
  // name: Zahra, age: 23
}


class Person {
  const Person({
    required this.name,
    required this.age,
  });

  final String name;
  final int age;
}

For more information, check docs.


Records (We can say tuples)

In some cases, we can encounter the case that we want to return multiple results from the method, or we want to parse JSON for only two or more fields. In these cases, we can use records. Let's look at the following examples.

We can simply define tuples with optional or named parameters and access with its name or $index+1 way.

void main() {
  var person = (name: 'Kanan', surname: 'Yusubov', age: 24);
  var person2 = ('Kanan', 'Yusubov', 24);

  print(person.age);
  print(person.surname);
  print(person2.$2);
}

We can use records and function parameters and return values:

You can use them as named parameters in functions too:

You can use it to parse some members of JSON:

void main() {
 final json = <String, dynamic>{
   'name': 'Kanan',
   'surname': 'Yusubov',
   'age': 24,
   'livesIn': 'Baku',
   'worksAt': ' Nasimi Bazari'
 };

  final (name, age) = getNameAndAge(json);
  print('$name: $age');
}

(String, int) getNameAndAge(Map<String, dynamic> json) {
  return (json['name'], json['age']);
}

For more information, check the docs.


Class Modifiers

Final Modifier

Using this keyword in the class declaration, it will guarantee:

  • Other classes can't extend or implement your class

  • Other classes can't use this class as a mixin

Look at the following example:

final class A {}

class B extends A {}
class C implements A {}
mixin D on A {}
class E with A {}

All of the above examples will give the syntax error. It guarantees that:

  • You can safely add new changes to your library, and API more safely

  • You can use the final class of any other library safely because it guarantees that it can not be extended or implemented in any other library.

But keep in mind that, you can extend and implement your final class within the same library. For that, you can set the subtype class as base, final or sealed.

final class A {}
final class B extends A {}
base class C implements A {}
sealed class D extends A {}

Interface Modifier

If you develop the new library, package and other things, within the same library, we can use the interface class as extended, and implemented. But if we use this library in another library, the interface class will allow us only to implement it.

So, using this modifier:

  • Your library will safely call the same implementation of instance methods within the same library.

  • Other libraries can't modify the base class definition for its instance methods. It will reduce the risk of fragile base class problem.

// Library a.dart
interface class A {
  void greet() { ... }
}

// Library b.dart
import 'a.dart';

var instanceOfA = A();       // Can be constructed

class B extends A {  // ERROR: Cannot be inherited
    int instanceMember;
    // ...
}

class C implements A {  // Can be implemented     
    @override
    void greet() { ... }
}

Base Modifier

Base modifier guarantees:

  • The base class constructor is called whenever an instance of a subtype of the class is created.

  • All implemented private members exist in subtypes.

  • A newly implemented member in a base class does not break subtypes, since all subtypes inherit the new member.

    • This is true unless the subtype already declares a member with the same name and an incompatible signature.

Let's explore the following example:

base class Person {
  void greet() {
    print('Hi!');
  }
}


class Programmer extends Person {

}

It will give the error like the following:

The type 'Programmer' must be 'base', 'final' or 'sealed' because the supertype 'Person' is 'base'.

The base modifier works like a reverse version of the interface keyword. You can only extend your base class in another library. But keep in mind that if you want to extend the base class, your subclass should define as a base or final class.


Sealed Modifier

It allows us to create an enumerable set of subtypes. Using the new exhaustive check feature We can use subtype classes in switch and switch expressions anywhere with more friendly syntax.

The sealed modifier prevents a class from being extended or implemented outside its library.

Sealed classes can't be instantiated, but they can contain factory constructors. So, it is implicitly abstract.

void main() {
  final developer = Developer();
  final programmer = Programmer();

  print(getTitle(developer));
  print(getTitle(programmer));
}

String getTitle(Person p) {
  return switch(p) {
      Developer() => 'Developer',
      Programmer() => 'Programmer',
  };
}

sealed class Person {}
class Developer extends Person {}
class Programmer extends Person {}

You can use sealed classes to handle different states in state management solutions (like Bloc, Riverpod, etc) in a more readable way:

void main() {
  final initial = Initial();
  final inProgress = InProgress();
  final failure = Failure('something went wrong!');
  final success = Success(10);

  print(handleState(initial));
  print(handleState(inProgress));
  print(handleState(failure));
  print(handleState(success));
}

String handleState(DataState state) {
  return switch(state) {
      Initial() => 'this is initial state',
      InProgress() => 'this is in progress state',
      Failure(message: var m) => 'error occured: $m',
      Success(data: var s) => 'success with $s',
  };
}

sealed class DataState {}

class Initial extends DataState {}
class InProgress extends DataState {}
class Failure extends DataState {
  Failure([this.message]);
  final String? message;
}
class Success extends DataState {
  Success(this.data);

  final int data;
}

Combining modifiers

You can follow the following order to add class modifiers:

  • in the first place, you can add the abstract keyword, but it is optional, it will allow the class to add some abstract members and will block the class to instantiate.

  • Then, you can add one of the following keywords interface, final, base, and sealed to add more rules for subclasses in other libraries. (it is also optional)

  • By the order, you can add a mixin keyword to add the capability for other classes to mix in. (optional)

  • and it is required to add the class keyword itself.

For some types, you can't combine. Keep in mind that:

  • You can't combine sealed and abstract classes, because the sealed class is implicitly abstract.

  • You can't use mixin with final,interface, and sealed, because they can't allow other libraries to use them as mixin.


I hope it will be informative article for you.

If you like, don't forget to subscribe and like.

1
Subscribe to my newsletter

Read articles from Kanan Yusubov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kanan Yusubov
Kanan Yusubov

I am Flutter Engineer based in Azerbaijan. I have more than 3 years of experience in this field. I am the founder of Azerbaijan Flutter Users Community. I am an open source fan. I often write different codes and share them openly. I think helping others is also helping yourself. The main reason I created the Azerbaijan Flutter Users Community is to help learners and share my knowledge. In my spare time, I write articles for Flutter Community on Medium. In addition, on my personal Youtube channel, I share technical materials.