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:
Declaration | Construct? | Extend? | Implement? | Mix in? | Exhaustive? |
class | Yes | Yes | Yes | No | No |
base class | Yes | Yes | No | No | No |
interface class | Yes | No | Yes | No | No |
final class | Yes | No | No | No | No |
sealed class | No | No | No | No | Yes |
abstract class | No | Yes | Yes | No | No |
abstract base class | No | Yes | No | No | No |
abstract interface class | No | No | Yes | No | No |
abstract final class | No | No | No | No | No |
mixin class | Yes | Yes | Yes | Yes | No |
base mixin class | Yes | Yes | No | Yes | No |
abstract mixin class | No | Yes | Yes | Yes | No |
abstract base mixin class | No | Yes | No | Yes | No |
mixin | No | No | Yes | Yes | No |
base mixin | No | No | No | Yes | No |
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.