Skip to content
← writing

August 4, 2025 · 3 min read

SOLID Principles in Dart & Flutter

Five principles for code that survives change, each with a practical Dart example — and why 'hard to test' usually means you're breaking one of them.

SOLID is five principles for writing code that survives change. They're not Flutter-specific, but they map cleanly onto Dart, and applying them is what keeps a growing app from turning into a pile of if statements. Here's each one with a Dart example.

S — Single Responsibility

A class should have one reason to change. When a widget fetches data, formats it, and paints it, every requirement touches the same file.

// Too many jobs: HTTP, parsing, and caching in one place.
class UserService {
  Future<User> load(String id) async { /* http + json + cache */ }
}

// Split by reason-to-change.
class UserApi { Future<Map<String, dynamic>> fetch(String id) => ...; }
class UserCache { Future<void> write(User u); Future<User?> read(String id); }
class UserRepository {
  UserRepository(this._api, this._cache);
  final UserApi _api;
  final UserCache _cache;
  Future<User> load(String id) async =>
      await _cache.read(id) ?? User.fromJson(await _api.fetch(id));
}

O — Open/Closed

Open for extension, closed for modification. You should add behavior without editing existing, tested code.

abstract interface class Discount {
  double apply(double total);
}

class PercentOff implements Discount {
  PercentOff(this.percent);
  final double percent;
  @override
  double apply(double total) => total * (1 - percent / 100);
}

class FlatOff implements Discount {
  FlatOff(this.amount);
  final double amount;
  @override
  double apply(double total) => (total - amount).clamp(0, total);
}

double checkout(double total, List<Discount> discounts) =>
    discounts.fold(total, (sum, d) => d.apply(sum));

Adding a BuyOneGetOne discount means writing a new class — not touching checkout.

L — Liskov Substitution

A subtype must be usable anywhere its supertype is expected, without surprises.

// Violation: Square silently breaks Rectangle's contract.
class Rectangle {
  Rectangle(this.width, this.height);
  double width, height;
  double area() => width * height;
}
// A Square that forces width == height breaks code that sets them independently.

The fix is usually composition or a shared abstraction (Shape with area()) rather than forcing an "is-a" relationship that doesn't hold.

I — Interface Segregation

Many small interfaces beat one fat one. Clients shouldn't depend on methods they never call.

// Fat: not every data source can do all of these.
abstract interface class Repository<T> {
  Future<T> read(String id);
  Future<void> write(T item);
  Future<void> sync();
}

// Segregated: implement only what a source supports.
abstract interface class Readable<T> { Future<T> read(String id); }
abstract interface class Writable<T> { Future<void> write(T item); }

D — Dependency Inversion

High-level code should depend on abstractions, not concrete details. This is the principle that makes testing and swapping implementations trivial.

// Domain layer defines the abstraction it needs.
abstract interface class AuthRepository {
  Future<User> signIn(String email, String password);
}

// Data layer provides the concrete implementation.
class SupabaseAuthRepository implements AuthRepository {
  SupabaseAuthRepository(this._client);
  final SupabaseClient _client;
  @override
  Future<User> signIn(String email, String password) async { /* ... */ }
}

// The use case depends on the interface — not on Supabase.
class SignIn {
  SignIn(this._repo);
  final AuthRepository _repo;
  Future<User> call(String email, String password) => _repo.signIn(email, password);
}

Wire the concrete type in at the edges with a service locator like get_it or a provider. In tests, inject a fake AuthRepository and your use case never knows the difference.

The payoff

SOLID isn't about ceremony — it's about isolating change. Get the seams right and new features slot in, bugs stay local, and tests become easy to write. That last point is the tell: if something is hard to test, it's usually violating one of these five.