Sitemap
Nisarg Ratani

Nisarg Ratani

Flutter & Node.js developer with 6+ years of experience. Published few articles on Medium, sharing practical insights on Flutter, Dart, and Node.js.

Dart Class Modifiers Explained: abstract, base, interface, final and sealed

6 min readSep 19, 2026

--

Introduction

Dart is an object-oriented programming language, and classes are at the heart of every Flutter application.

As your codebase grows, you need better control over:

  • Inheritance.
  • Implementation.
  • Extensibility.
  • API design.
  • Architectural boundaries.
  • Type-safe state modeling.

Dart provides class modifiers that help you define how classes can be extended or implemented.

The most important modifiers are:

  • abstract
  • base
  • interface
  • final
  • sealed

In this article, we’ll explore each modifier with practical examples and Flutter architecture use cases.

1. Why Class Modifiers Matter

Consider a shared class in a Flutter package.

class ApiClient {
void request() {
// Implementation
}
}

Other developers can potentially:

  • Extend the class.
  • Implement the class.
  • Override its methods.
  • Create alternative implementations.

Sometimes this flexibility is useful.

Sometimes you want to restrict it.

Class modifiers allow you to communicate and enforce your intended API design.

2. The Abstract Modifier

An abstract class cannot be directly instantiated.

abstract class Animal {
void makeSound();
}

You cannot write:

// final animal = Animal(); // Invalid

Instead, create a subclass:

class Dog extends Animal {
@override
void makeSound() {
print('Bark');
}
}

Usage:

final dog = Dog();
dog.makeSound();

Abstract Methods

An abstract method has no implementation in the abstract class.

abstract class Shape {
double get area;
}

Subclass:

class Circle extends Shape {
final double radius;

Circle(this.radius);
@override
double get area => 3.14 * radius * radius;
}

Abstract classes are useful when you want to define a common contract or share implementation among subclasses.

3. Abstract Classes in Clean Architecture

Abstract classes are frequently used for repository contracts.

Domain Layer

abstract class UserRepository {
Future<User> getUser(String id);
}

Data Layer

class UserRepositoryImpl
implements UserRepository {
final UserRemoteDataSource remoteDataSource;

UserRepositoryImpl({
required this.remoteDataSource,
});
@override
Future<User> getUser(String id) {
return remoteDataSource.getUser(id);
}
}

The domain layer defines the contract, while the data layer provides the implementation.

Note: An abstract class can also be implemented by another class. If you specifically want a contract-only type, abstract interface class is often clearer.

4. The Interface Modifier

Dart classes implicitly define interfaces, but the interface modifier provides additional control.

interface class Logger {
void log(String message) {
print(message);
}
}

An external library cannot extend this class, but it can implement it.

class AppLogger implements Logger {
@override
void log(String message) {
print('[APP] $message');
}
}

What Does Interface Prevent?

A class marked interface cannot be extended outside its defining library.

This lets you expose a contract without allowing external inheritance.

Abstract Interface Class

For a pure contract, use:

abstract interface class Logger {
void log(String message);
}

Implement it:

class ConsoleLogger implements Logger {
@override
void log(String message) {
print(message);
}
}

This combination is especially useful for repository and service abstractions.

5. The Base Modifier

A base class requires all direct or indirect subclasses to be declared with a compatible inheritance relationship.

base class Vehicle {
void move() {
print('Moving');
}
}

A subclass:

base class Car extends Vehicle {
void drive() {
print('Driving');
}
}

A class in another library cannot implement a base class.

Why Use Base?

Use base when you want to provide implementation that subclasses inherit, while preventing external classes from implementing your class as an interface.

This protects assumptions made by the base class.

Important

A class that extends a base class must itself respect the inheritance restrictions. For example, it may be declared base, final, or sealed, depending on the desired hierarchy.

6. The Final Class Modifier

A final class cannot be extended or implemented outside its defining library.

final class PaymentService {
void pay() {
print('Processing payment');
}
}

External code cannot create a subclass of PaymentService.

Why Use Final?

Use final when:

  • You don’t want external inheritance.
  • You want to preserve implementation flexibility.
  • You want to expose a stable public API.
  • You want to prevent external implementations.

Example:

final class AppConfig {
final String environment;

const AppConfig({
required this.environment,
});
}

This is suitable for classes whose implementation should remain under the library author’s control.

7. The Sealed Modifier

A sealed class is useful for representing a restricted hierarchy of types.

sealed class Result<T> {
const Result();
}

Subclasses:

class Success<T> extends Result<T> {
final T data;

const Success(this.data);
}
class Failure<T> extends Result<T> {
final String message;
const Failure(this.message);
}

You cannot directly instantiate the sealed class.

Get Nisarg Ratani’s stories in your inbox

Join Medium for free to get updates from this writer.

The compiler knows the direct subtypes in the library and can use that knowledge for exhaustive switching.

8. Sealed Classes in Flutter State Management

Sealed classes are excellent for modeling UI states.

sealed class UserState {
const UserState();
}

class UserInitial extends UserState {
const UserInitial();
}
class UserLoading extends UserState {
const UserLoading();
}
class UserLoaded extends UserState {
final User user;
const UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
const UserError(this.message);
}

Now handle states:

Widget buildUserState(
UserState state,
) {
return switch (state) {
UserInitial() =>
const Text('Start'),

UserLoading() =>
const CircularProgressIndicator(),
UserLoaded(:final user) =>
Text(user.name),
UserError(:final message) =>
Text(message),
};
}

This design avoids relying on combinations of boolean flags such as:

bool isLoading;
bool hasError;
bool hasData;

A sealed hierarchy represents distinct states more clearly.

9. Comparing Class Modifiers

Press enter or click to view image in full size

These modifiers serve different purposes. Choose based on how you want other code to use your class.

10. Abstract vs Interface

This is an important distinction.

Abstract Class

abstract class Repository {
Future<void> save();
}

It can be extended or implemented, subject to normal library rules.

Abstract Interface Class

abstract interface class Repository {
Future<void> save();
}

It is intended to define a contract and cannot be extended outside its defining library.

Use abstract interface class when you want a clear interface boundary.

11. Base vs Final

Both base and final prevent external implementation.

The main difference is that a base class is designed to support inheritance, while a final class prevents external inheritance and implementation.

Base

base class BaseRepository {
void log() {
print('Repository');
}
}

Intended for a controlled inheritance hierarchy.

Final

final class UserRepository {
void fetch() {
print('Fetching');
}
}

Intended to prevent external subclassing or implementation.

12. Sealed vs Final

A final class prevents external extension and implementation, but it does not provide the same sealed hierarchy semantics for exhaustive switching.

A sealed class is designed to model a known family of subtypes.

Use:

  • final for classes that should not be extended externally.
  • sealed for restricted type hierarchies and exhaustive pattern matching.

13. Class Modifiers and Library Boundaries

One important detail: these restrictions apply relative to the library where the class is declared.

Dart libraries are not necessarily the same as individual files.

If multiple files are part of the same library, modifier restrictions should be understood in the context of that library boundary.

For modern Dart applications, use clear library organization and verify behavior with the Dart version used by your project.

14. Choosing Modifiers in Flutter Architecture

Repository Contract

abstract interface class UserRepository {
Future<User> getUser(String id);
}

This is suitable when you want a contract that implementations fulfill.

Controlled Base Service

base class BaseService {
void logRequest() {
print('Request');
}
}

Useful when you control the inheritance hierarchy.

Public Utility Class

final class DateUtilsService {
const DateUtilsService();

String format(DateTime date) {
return date.toIso8601String();
}
}

Useful when external subclassing is not part of your API design.

UI State

sealed class LoginState {
const LoginState();
}

Useful for state modeling and exhaustive switching.

15. Common Mistakes

Mistake 1: Using Abstract for Every Contract

An abstract class can define both a contract and shared implementation.

If you want to prohibit external extension, use an appropriate interface modifier.

Mistake 2: Using Final When You Need a State Hierarchy

A final class is not a replacement for a sealed base type.

Choose sealed when you need multiple related state variants.

Mistake 3: Assuming Sealed Means No Subclasses

Sealed classes can have subclasses. Their purpose is to control the hierarchy and support exhaustive type analysis.

Mistake 4: Ignoring Library Boundaries

Class modifiers are library-oriented. Understand which classes belong to the same library before drawing conclusions about inheritance restrictions.

Mistake 5: Overusing Modifiers

Do not add modifiers just because they exist.

Choose the least restrictive design that correctly communicates your intended API.

16. Best Practices

  • Use abstract interface class for contract-oriented APIs.
  • Use base when controlled inheritance is intentional.
  • Use final for classes that should not be extended or implemented externally.
  • Use sealed for finite state hierarchies.
  • Use abstract when direct instantiation should be prohibited.
  • Document public APIs and their intended extension points.
  • Validate modifier behavior against your Dart SDK version.

Conclusion

Dart class modifiers give Flutter developers more control over object-oriented design.

The key takeaways are:

  • abstract: Define non-instantiable abstractions.
  • base: Control external implementation while supporting inheritance.
  • interface: Expose contracts without allowing external extension.
  • final: Prevent external inheritance and implementation.
  • sealed: Model restricted hierarchies and support exhaustive switching.

Using these features thoughtfully helps you build maintainable Flutter applications, reusable packages, and stronger architecture boundaries.

Good architecture is not just about organizing files. It’s also about controlling how your types can evolve.

--

--

Nisarg Ratani
Nisarg Ratani

Flutter & Node.js developer with 6+ years of experience. Published few articles on Medium, sharing practical insights on Flutter, Dart, and Node.js.