Dart 3 Class Modifiers Explained With Examples.

Introduction

Dart is a programming language that is permissive by default, it does offer great flexibility and power to its community of developers. Now, Dart's version 3 is on the stable channel, this one comes with many new specifications & features such as patterns, records, and class modifiers...

In this article, we will cover the new class modifiers that will definitely bring more value to you as a Dart/Flutter developer.

What Are Class Modifiers?

If you worked previously with Dart programming language, you might be familiar with declaring classes directly with the class keyword like in this example:

class MyClassA {}

or declaring an abstract class to declare an interface with the abstract keyword, and then implement it with the implements like this example:

abstract class MyClassB {}
class MyClassC implements MyClassB {}

The thing that you should know, is that implementing a non-abstract class in your Dart application will cause a compile-time error unless it contains all the members definitions of the class you claim to implement.

Now let's take an example of an abstract class that will be implemented in many other classes:

abstract class AbstractClassA {
 abstractMethod();
}

class MyFirstClass implements AbstractClassA {} // Error.
class MySecondClass implements AbstractClassA {} // Error.

you might know already why the error should happen, right? if not, then it is because as we said, classes that implement an abstract class should define all members of it:

abstract class AbstractClassA {
 abstractMethod();
}

class MyFirstClass implements AbstractClassA {
  @override
  abstractMethod() => "randome text";
} // Ok.

class MySecondClass implements AbstractClassA {
  @override
  abstractMethod() => "randome text";
} // Ok.

Now imagine if we want to define another abstract method in the AbstractClassA:

abstract class AbstractClassA {
 abstractMethod();
 anotherAbstractMethod();
}

You might guess what will happen, right? Yes sir. now a compile error will be thrown in our code, informing us that MyFirstClass and MySecondClass should define that new method as well.

This can be a serious problem when working on a Dart/Flutter package, app.. since anyone can implement your class on their own and use it. And then every newly declared abstract method in AbstractClassA will literally require a breaking change in your code as well to implement, and define it as well.

In such cases, Dart/Flutter packages developers rely on informing their packages users via documentation, comments documentation saying something like:

Please, don't implement this class. it is intended to be used only in the package internally and not outside.

and then relies on the package users to read it and not do it.

But this is not enough at all, we need a way to guarantee that no one can do a specific action on a specific class, right?

New Class Modifiers

The previous example was one of many cases that can lead to similar issues during extending, implementing, constructing, and mixing a class in other code.

And here, the new class modifiers come to play, we will explain each one in its own section ad an example use case:

The final Class Modifier

The new final class modifier that can be declared like:

final class FinalClass {}

will allow the FinalClass to be only constructed and prevent all other actions such as implementing and extending it or using it as a mixin class, so trying:

// All following lines will throw an error, a final class can only be constructed
class Class1 extends FinalClass {}
class Class2 implements FinalClass {}
class Class3 with FinalClass {}
mixin Mixin on FinalClass {}
enum Enum implments FinalClass {}

Will simply throw a compile error informing you that FinalClass is a final class and so it is intended to only be constructed:

var myFinalClass = FinalClass(); // OK, we can do this only.

This is useful if you have a class that you want to use only within your library/package, and so, no one else can build on top of it or reflect it, it will simply prevent all of that.

The interface Class Modifier

First, to showcase the interface class modifier, we will declare two Dart librarieslib_a and lib_b.

inside the lib_b, we will create a new simple class B:

library lib_b;

class B {
  void doSomething() {
    print('Doing something');
  }
}

This is fine, we have now a library that contains only one class which is B, now by marking that class with the new interface class modifier:

library lib_b;

interface class B {
  void doSomething() {
    print('Doing something');
  }
}

It will allow the B class to be used within the same library ( lib_b ) without any restrictions, this means that we can do:

library lib_b;

interface class B {
  void doSomething() {
    print('Doing something');
  }
}

class BB extends B {
  @override
  void doSomething() {
    print('Doing something else');
  }
}

And it will work just fine since we still working within the same library, so for you as a developer, you probably would not feel the difference of it, But now if we import the lib_b library inside another library which is the lib_a, and then trying to extend the B class, this will be prevented. The interface allows a class to be implemented, but not extended:

library lib_a;

import 'lib_b.dart';

// Error, we can't extend the B class since it is an interface.
class A extends B {
  @override
  void doSomething() {
    print('Doing something else');
  }
}

// Ok, the B is marked with interface, and so we can implement it normally outside it's library.
class AA implements B {
  @override
  void doSomething() {
    print('Doing something else');
  }
}

as you can see, the interface class modifier will prevent extending a class from outside the class. with this, you can work on your package APIs & methods safely and guaranteeing that externally, everyone can implement that class but no one can extend its same functionality.

The base Class Modifier

Taking the same libraries lib_b and lib_a notations as from the previous section.

The new base class modifier that can be declared like this:

library lib_b;

class B {
  void doSomething() {
    print('Got it.');
  }
}

This is fine, we declared a simple B class inside the lib_b library, now by marking it with that new base class modifier:

library lib_b;

base class B {
  void doSomething() {
    print('Got it.');
  }
}

it will allow the B class to be only extended but not implemented, you can say that the base class does the reverse of the interface (when talking about extending, or implementing a class ), and so if we did:

library lib_a;

import 'lib_b.dart';

// Error, we can't implement a base class externally.
base class ClassA implements B {
  @override
  void doSomething() {
    print('Got it from ClassA.');
  }
} 

// OK, a base class (BaseClassB) can be extended by another class (AnotherClassA)
base class AnotherClassA extends B {
  @override
  void doSomething() {
    print('Got it from ClassA.');
  }
}

as you can see here, implementing a base class is prevented and will throw a compile-time error, you can only extend it.

Note:

Please note that if a class does extends a base class, then it requires it to a base or final class.

mixin Class Modifier

Before introducing the mixin class modifier, in previous versions of Dart, developers could declare a class that can be used as a mixin normally like this:

class MixinClass {} 

class AnotherClass with MixinClass {} // previously, this was OK.

Normal classes were able to be used as mixins to other classes directly, this leads to a little confusion since Dart already has the mixins feature shipped separately.

But now with Dart 3, in order to use a class as a mixin, you will have to mark it with the mixin class modifier keyword, otherwise it will throw a compile-time error:

class MixinClass {} 

class AnotherClass with MixinClass {} // Error, classes that are not marked with mixin class modifier can't be used as mixins anymore.

And so, in order to resolve the issue, you will need to simply tell the compiler that this class is intended to be used as a mixin:

mixin class MixinClass {}

class AnotherClass with MixinClass {} // Ok, this now is good with Dart 3.

Using Multiple Class Modifiers

The new class modifiers that have been explained above can be combined with each other and with other Dart abstract class modifier that is intended to prevent the construction of a class.

For example, let's say that we want to declare a class that can be constructed and that allows it to be extended only, if the base class modifiers are what you're thinking about right now, then good job:

base class ClassA {}

what if I asked you to make a class that allows being extended and implemented but prevent being constructed, the answer would be the abstract class modifier, right? :

abstract class ClassB {}

Now here is the interesting thing, what if I asked you to make a class that is able to be only extended, this means that it can't be constructed or implemented, here we can combine both the base and the abstract modifiers, and so we will have a result like:

abstract base class OnlyExtendableClass {}

in the same manner, you could combined class modifiers to achieve specific behaviors of classes that match your use case, here is a table that showcases possibilities:

DeclarationConstruct?Extend?Implement?Mix in?Exhaustive?
classYesYesYesNoNo
base classYesYesNoNoNo
interface classYesNoYesNoNo
final classYesNoNoNoNo
sealed classNoNoNoNoYes
abstract classNoYesYesNoNo
abstract base classNoYesNoNoNo
abstract interface classNoNoYesNoNo
abstract final classNoNoNoNoNo
mixin classYesYesYesYesNo
base mixin classYesYesNoYesNo
abstract mixin classNoYesYesYesNo
abstract base mixin classNoYesNoYesNo
mixinNoNoYesYesNo
base mixinNoNoNoYesNo

Source: Class Modifiers Specification - Syntax

Conclusion

Dart 3 comes with many interesting features that will definitely help its developers to create and ship more reliable apps, the new class modifiers are intended to prevent some common issues and enforce consistency when working or contributing to the packages ecosystem of Dart & Flutter.