avatarRei

Summary

The provided web content discusses the new class modifiers introduced in Dart 3.0, which include sealed, base, interface, and final classes, and explains how they provide more control over class usage and prevent misuse.

Abstract

Dart 3.0 introduces class modifiers that enhance the control developers have over class behavior and interactions. These modifiers—sealed, base, interface, and final—restrict the ability to construct, inherit, or implement classes in specific contexts, thereby reducing the potential for bugs and unexpected behavior. The article delves into the practical implications of these modifiers with examples, demonstrating how they can be used to enforce design patterns and prevent API misuse. It also provides a comprehensive table summarizing the capabilities and restrictions of each class modifier, aiming to clarify their roles for developers transitioning to Dart 3.0.

Opinions

  • The author acknowledges the initial confusion surrounding the new class modifiers but emphasizes their utility in improving code reliability and maintainability.
  • The article suggests that the ability to restrict class usage is crucial for maintaining the integrity of APIs and preventing unintended implementations.
  • The author expresses enthusiasm about the sealed class modifier, highlighting its effectiveness in ensuring exhaustiveness in pattern matching, which is particularly useful in state management.
  • There is an appreciation for the clarity that base and interface class modifiers bring to class hierarchies, making the intent and capabilities of classes more explicit.
  • The author points out that the final class modifier is essential for creating classes that should not be subclassed or implemented, thus enforcing a more rigid API contract.
  • The article subtly criticizes the flexibility of previous Dart versions, implying that the new class modifiers address long-standing issues with class hierarchies and API design.
  • The author encourages readers to embrace these changes, suggesting that while the learning curve may be steep, the benefits of the new modifiers will become apparent with time and practice.

Dart 3.0 features in-depth

Class modifiers in Dart 3.0: abstract, interface, base, and sealed. OH MY!

Yes! We’re all confused! but this article may help you understand a bit better!

Class modifiers are one of the biggest features in the Dart 3. They are pretty handy, but also confusing. Let’s discuss why we need them and how we can use them with comprehensible examples!

But, before we get into modifiers, I would like to mention extends, implements, and with keywords for a better understanding of the topic.

What is “extends”?

extends is a keyword to inherit a class from another.

In simple terms, you have children, and they get your DNA, But the older one is a quiet child and the younger one is pretty hyperactive. Even though they are like you, they have their own personalities.

This is exactly what StatelessWidget and StatefulWidget do to Widget class.

abstract class Widget {}

abstract class StatelessWidget extends Widget {}
abstract class StatefulWidget extends Widget {}

So, they inherit their features from their parents, but they can override some of them and build their own features.

Terminology Note!

class B extends A {}

class A is AKA parent, base, or super class.

class B is AKA child, derived, heir, or subclass.

abstract class Interceptor {
  void onRequest() => log('Parent Request');
  void onResponse() => log('Parent Response');
  void onError() => log('Parent Error');
}

// We only want to override the requests
// others can do as they want
class TokenInterceptor extends Interceptor {
  @override
  void onRequest() => log('Child Request with Token');
}

void main() {
  final Interceptor a = TokenInterceptor();
  a.onRequest(); // [log] Child Request with Token
  a.onError(); // [log] Parent Error
}

But what if we want to override everything in the class?

What is “implements”?

implements is a keyword used to redefine a class

In simple terms, imagine that you kidnap someone, lock him in a room, take his identity, pretend to be him, and start living his life.

This is exactly what Element does to BuildContext.

abstract class BuildContext {}

abstract class Element implements BuildContext {}

I think, we can say;

Implements: Exact copy (have to override all properties)

Extends: Derived Copy (overrides properties that wishes to)

import 'dart:developer' as developer;

abstract class Logger {
  void log(String text);
  late final int level;
}

class ErrorLogger implements Logger {
  @override
  int level = 3;

  @override
  void log(String text) => developer.log(text, name: 'ERROR', level: level);
}

void main() {
  final Logger logger = ErrorLogger();
  logger.log('ehe'); // [ERROR] ehe
}

What is “with”?

with is a keyword used to add additional superpowers to a class.

In simple terms, Let’s assume you’re a human and want to fly but you can’t inherit your flying power from a bird class. Instead, you need extra equipment to fly! (Wings!)

This is exactly what SingleTickerProviderStateMixin does to State. (Ticker!)

mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider  {}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {}
mixin PrettyJsonMixin {
  Map<String, Object?> toJson();

  @override
  String toString() {
    return JsonEncoder.withIndent(' ' * 2).convert(toJson());
  }
}

// Outside of Library
class User with PrettyJsonMixin {
  const User({required this.name, required this.friends});

  final String name;
  final List<Map<String, String>> friends;

  @override
  Map<String, Object?> toJson() => {'name': name, 'friends': friends};
}

void main() {
  final user = User(
    name: 'ehe',
    friends: [
      {'name': 'evet'},
      {'name': 'oly'},
      {'name': 'be'},
    ],
  );
  print(user);
}

// prints
{
  "name": "ehe",
  "friends": [
    {
      "name": "evet"
    },
    {
      "name": "oly"
    },
    {
      "name": "be"
    }
  ]
}

Let’s take a look at the class modifiers now!

Class Modifiers!

What are class modifiers, and why do we need them?

Short answer:

They are keywords to get more control over the classes and prevent unexpected bugs and behaviors by removing some capabilities of the API.

Long answer:

What are class modifiers?

Class modifiers are keywords to remove some of the capabilities of a class.

But why do we remove capabilities?

The answer is simple. Restrict the user from misusing the API.

What do you mean by misusing?

Classes can be constructed, inherited, and implemented by default. Even though it gives us flexibility, it also brings some disadvantages.

For example, assume we made a print package and it looks like this;

/// Usage: Print.error('your message');
abstract class Print {
  static void error(String message) => print('ERROR! $message');
}

It’s looking great! But still have some problems with it.

For example, some people will try to use them in ways we don’t expect.

// Outside of Library

final myPrint = Print();

class CustomPrint implements Print {}

class AnotherPrint extends Print {}

So, what could we do before Dart 3?

We could write a comment and kindly warn others not to use them improperly (extend, implement, or construct).

But it wouldn’t be a sufficient way to prevent the problem. Because most people don’t pay much attention to comments, someone will eventually do that.

So, what changed in Dart 3?

We have class modifiers to prevent these kinds of problems now!

Let me explain the class modifiers one by one, with some examples.

Note: Do not forget that these rules are all valid if you’re trying to use them from outside this library

sealed class

You cannot usethis class in any way outside of the library

  • Construct 🚫
  • Inherit 🚫
  • Implement 🚫
class State {
  const State._();
  factory State.success(String foo) = SuccessState;
  factory State.error(String foo) = ErrorState;
}

class ErrorState extends State {
  const ErrorState(this.msg) : super._();
  final String msg;
}

class SuccessState extends State {
  const SuccessState(this.value) : super._();
  final String value;
}

If we don’t have the sealed class, what would happen?

// Outside of Library

class CustomState implements State {}

void main() {
  whatIsThisState(CustomState());
}

void whatIsThisState(State state) {
  switch (state) {
    case ErrorState():
      log(state.msg);
    case SuccessState():
      log(state.value);
    default:
      log('This case should not exist!');
  }
}

“Oh, Snap! I only expected Error or Success but we got another state! This shouldn’t have happened!” — Someone before sealed class exists

Fortunately, we have sealed classes now and we can prevent that happened

// sealed inside of the library
sealed class State {
  ...
}

// Outside of Library 

// Error!
// The class 'State' can't be extended, implemented, or mixed in
// outside of its library because it's a sealed class.
class CustomState implements State {}

We can make sure that there are no other states now!

base class

You cannot implement a class outside of the library

  • Construct ✅
  • Inherit ✅
  • Implement 🚫

In this case, we don’t want to override id variable every time.

That’s why, we used a base class, not to implement it.

base class BaseModel {
  const BaseModel(this.id);
  final int id;
}

// Outside of Library
final class User extends BaseModel {
  final String name;
  final String surname;

  const User(super.id, this.name, this.surname);
}

// Error! You should inherit it!
final class Product implements BaseModel {
  final String name;
  final String category;

  const Product(super.id, this.name, this.category);
}

interface class

You cannot inherit this class outside of the library

  • Construct ✅
  • Inherit 🚫
  • Implement ✅

We want to prevent unimplemented methods in the Storage class.

// abstract -> can extend and implement
// interface -> can construct and implement
// abstract interface -> can only implement
abstract interface class Storage {
  void save(String key, String value);
  String load(String key);
}

// Outside of Library
class IsarStorage implements Storage {
  @override
  String load(String key) => 'data loaded from $key';

  @override
  void save(String key, String value) => '$value saved to $key';
}

// Error!
// Make sure that class is implemented, not extended!
class HiveStorage extends Storage {
  @override
  String load(String key) => 'data loaded from $key';

  // Where is the save method? You should implement this class!!
}

final class

You cannot either inherit or implement this class outside of the library.

That means we have no choice but to create an instance and use it.

  • Construct ✅
  • Inherit 🚫
  • Implement 🚫

In this case, we don’t want to create a subclass for AuthException.

That’s why we restrict all use of AuthException.

final class AuthException implements Exception {
  AuthException(this.message, [this.code, this.stackTrace]);

  final String message;
  final int? code;
  final StackTrace? stackTrace;

  @override
  String toString() => message;

  factory AuthException.login([int? code, StackTrace? stackTrace]) {
    return AuthException('email or password is not correct', code, stackTrace);
  }
}

// Outside of Library

// Error!
// Cannot extends or implement this class
class LoginException extends AuthException {}

mixin class

You can add mixin capability to a class

  • Construct ✅
  • Inherit ✅
  • Implement ✅
  • mix in ✅

Although it is rare to define a mixin class, there is a class we use a lot that uses mixin class, and its name is ChangeNotifier!

Fun Fact: You can check all classes that use mixin modifiers in the Flutter framework here

// Pseudocode
abstract class Listenable {
  void addListener(void Function() listener);
  void removeListener(void Function() listener);
}

mixin class ChangeNotifier implements Listenable {
  @override
  void addListener(void Function() listener) {}

  @override
  void removeListener(void Function() listener) {}
}


// Outside of Library

class Controller extends ChangeNotifier {} // valid

class AnotherController with ChangeNotifier {} // valid

Note! It was possible to define a class to both inherit and mix in without using a mixin modifier, but classes can no longer mix in without using mixin keyword.

/// Dart 2.19.6
class ChangeNotifier {}

class Controller extends ChangeNotifier {} // Valid
class AnotherController with ChangeNotifier {} // Valid


/// Dart 3.0
class ChangeNotifier {} // without mixin 

class Controller extends ChangeNotifier {} // Valid
class AnotherController with ChangeNotifier {} // Error

mixin class ChangeNotifier {} // with mixin 

class Controller extends ChangeNotifier {} // Valid
class AnotherController with ChangeNotifier {} // Valid

Lastly, here is the full table of the combinations of all classes!

Good luck!

It’s kind of confusing, actually, but I believe we’re going to get used to it in time.

Other Articles For New Features

References

Thank you for reading so far!

It was my journey with modifiers. In this article, I wanted to talk about why to do it, not how to do it. I hope it was useful for you too.

If you like this article, click on the 👏 button (do you know it can go up to 50?)

One last thing! You can support my articles with my referral link, if you’d like to!

Have a fluttery day! 👋

Flutter
Dart
New Features
Technology
Programming
Recommended from ReadMedium