Class Modifiers in Dart: Sealed, Interface, Base

With the release of Dart 3, class modifiers finally got some changes that try to make them more convenient and powerful. Dart introduced the new class modifiers sealed, interface, final, and base but also made some changes to how existing ones behave. The modifications spawned some confusion among users making class modifiers seem to be more complicated and harder to understand than before.

Are those changes a real improvement for Flutter and Dart? In this article, we will bring some light to this and explore the changes, reasons as well as use-cases to use those new modifiers.


The Problem

Before the Dart 3 update, the usage of class modifiers in Dart presented a few issues:

  • If you come from another OOP language you would not understand how you can define an interface
  • All classes can be “mixed in” into other classes, besides an extra modifier (mixin) exists for that
  • Sealed and final classes don’t exist which makes pattern-matching (also a new feature in Dart 3) impossible

With Dart 3, these problems are addressed with the introduction of new modifiers and adjustments to the existing ones. To developers who have already explored the new Dart 3 class modifiers, they might seem more intricate than the previous ones. This post aims to clarify these new modifiers and provide practical use cases for them. Let’s begin by reviewing what class modifiers are and have a look at the old and new ones.

Class Modifiers 📦

Class modifiers are the keywords that you use to define a class and remove specific characteristics from it. Class modifiers define whether a type can be extended, mixed-in, constructed or implemented into another class.

class

If you don’t apply any modifier to a class it follows the default behavior. It can be constructed as a simple class and can be extended and implemented by any other classes.

abstract class

An abstract class does not require a concrete implementation. It cannot be constructed and can define abstract methods, which also do not need an implementation. If a class (which is not abstract) extends an abstract class it needs to implement the provided interface of the abstract class. Abstract classes cannot be “mixed in” as a mixin with the with keyword.

mixin / mixin class

A mixin declaration defines a mixin that can be “mixed in” into another class. If you need a refresher on what mixins are have a look at our article here. By default mixins do not have the same abilities as a class, therefore you can also combine them and define a  mixin class. This declaration defines a class that is usable as both a regular class and a mixin, with the same name and the same type. Any restrictions that apply to classes or mixins also apply to mixin classes:

  • Mixins can’t have extends or with clauses, so neither can a mixin class.
  • Classes can’t have an on clause, so neither can a mixin class.

base class 🆕

Base classes were introduced to enforce inheritance of a class or mixin’s implementation. Just use the base modifier in front of class or mixin. A base class disallows implementation outside of its own library. This 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.

Only the base modifier can appear before a mixin declaration. You must mark any class which implements or extends a base class as base, final, or sealed. This prevents outside libraries from breaking the base class guarantees.

interface class 🆕

By default, every class in Dart also creates an interface implicitly. If you were unaware of that check out the documentation from the Dart team about that. Since that’s not obvious for newcomers, Dart 3 introduced the interface modifier. Libraries outside of the interface’s own defining library can implement the interface, but not extend it. This guarantees:

  • When one of the class’s instance methods calls another instance method on this, it will always invoke a known implementation of the method from the same library.
  • Other libraries can’t override methods that the interface class’s own methods might later call in unexpected ways. This reduces the fragile base class problem

Most of the time you will use an abstract interface since that’s the behavior you would expect if you come from another programming language. because by default interfaces in Dart are constructible.

final class 🆕

To close the type hierarchy, use the final modifier. This prevents subtyping from a class outside of the current library. Disallowing both inheritance and implementation prevents subtyping entirely. This guarantees:

  • You can safely add incremental changes to the API.
  • You can call instance methods knowing that they haven’t been overwritten in a third-party subclass.

Final classes can be extended or implemented within the same library. The final modifier encompasses the effects of base, and therefore any subclasses must also be marked base, final, or sealed.

sealed class 🆕

To create a known, enumerable set of subtypes, use the sealed modifier. This allows you to create a switch over those subtypes that is statically ensured to be exhaustive.

The sealed modifier prevents a class from being extended or implemented outside its own library. Sealed classes are implicitly abstract.

  • They cannot be constructed
  • They can have factory constructors
  • They can define constructors for their subclasses to use

Subclasses of sealed classes are, however, not implicitly abstract. The compiler is aware of any possible direct subtypes because they can only exist in the same library. This allows the compiler to alert you when a switch does not exhaustively handle all possible subtypes in its cases.

Here is an example of how you can use the benefits of sealed classes in code:

sealed class Animal {}

class Bird extends Animal {}

class Fish extends Animal {}

void findAnimal(Animal animal) {
  switch (animal) {
    case Bird _:
      print('Bird');
      break;
    case Fish _:
      print('Fish');
      break;
  }
}

We can do an exhaustive switch over the class and don’t have to deal with a default case. We have the guarantee that at any time an Animal is either a Bird or a Fish.

Library 📚

Libraries are a chunk of code and classes that you can (or someone else) can put together that can be used inside your project. There are two ways to define a library.

  1. Create a Dart/Flutter package/plugin that can be imported with the pubspec.yaml. Your code will be bundled in there and e.g sealed classes cannot be extended outside of the original code
  2. You can also define a library inside your own code by using the library keyword. Normally you use barrel files to put together some classes. That way you can easily export some specific classes and they can be imported altogether by this one file. Only classes mentioned inside the barrel file can extend the sealed class; classes from the outside cannot do this.

Combination 🔗

Class modifiers cannot only be used individually but can also be used in combination with one another. abstract base mixin class is an absolute valid modifier and it means that the class cannot be constructed or implemented and can only be used outside the library through inheritance.

Adding other class modifiers (interface, final, sealed) than abstract before the mixin, doesn’t make sense since that would block the mixin from being used with the with keywords. That’s why it’s prohibited.

Redundancies ➰

Since some of the class modifiers share some of their characteristics they can be redundant and you can get rid of some of them or combine them too another one. Here is a list of some of those redundancies. The linter will help you with that and even shows a warning when using the wrong keywords together.

sealed & abstract -> Drop abstract.
interface & final -> Drop interface.
base & final -> Drop base.
interface & base -> Say final instead.
sealed & final -> Drop final.
sealed & base -> Drop base.
sealed & interface -> Drop interface.
abstract & mixin -> drop abstract.
Just interface (Compiler will not allow) -> abstract class.

Complexity 🤯

Ok that was a lot and I don’t what you are thinking but to us, this implementation of class modifiers looks overwhelming, and it’s hard to wrap your head around all the possible implementations or even to find the correct list of class modifiers that are needed for your use-case.

BUT because there are redundancies it’s possible to put a list together of all possible class modifier combinations. There are 15 different combinations right now on how class modifiers can be used alone or in combination. We focused on the abilities that the class / mixin modifier gives/removes from a class. For extending and implementing we are thinking as the class would be implemented outside of the library.

Don’t forget there is a difference if the class is used inside or outside a library.

Declaration Construct? Extend? Implement? Mixin? Exhaustive?
class
base class
interface class
final class
sealed class
abstract class
abstract base class
abstract interface class
abstract final class
mixin class
base mixin class
abstract mixin class
abstract base mixin class
mixin
base mixin

(Thanks to the Dart team for creating that list)

Why? 🤔

Let’s get back to our initial question: Did the changes to class modifiers improve the language or did it just make the language more complicated?

In our opinion yes and no! Here’s why:

The new class modifiers help to make your code more concise and especially if you are a package maintainer they help to prevent misuse of your package. By introducing sealed classes, Dart now allows pattern-matching. Class modifiers are so precise that almost any imaginable use-case is mappable, but that is also coming at a cost.

All those class modifiers make the language more complicated and harder to understand. Especially newcomers to the Dart/Flutter platform can feel overwhelmed since there are so many different keywords that can be used in combination and some also don’t even behave the same as their counterparts in other languages. Aswell for experienced Dart and Flutter developers those changes seem to be overwhelming at the first look.

Conclusion

In conclusion, class modifiers are a powerful tool that can be used to improve the readability, maintainability, and safety of your Dart code. By using class modifiers, you can make it clear to other developers what your intentions are for a particular class, prevent accidental modifications to classes, and prevent the creation of unexpected subclasses.

The Dart team offers a migration guide for plugin/package maintainers. Feel free to check it out if you need additional help after this article.

Did you enjoy this article? Tell us your opinion! And if you have any open questions, thoughts, or ideas, feel free to get in touch with us! Thanks for reading! 🙏

Share via
Copy link
Powered by Social Snap

Get notified when our next article is born!

(no spam, just one app-development-related article
per month)