When building Flutter applications, it’s easy to get caught up in writing code that just works. But as your app grows in size and complexity, poorly structured code becomes harder to maintain, test, and extend. That’s where the SOLID principles come in.
SOLID is an acronym for five design principles that help developers write clean, scalable, and maintainable code:
S – Single Responsibility Principle (SRP)
O – Open/Closed Principle (OCP)
L – Liskov Substitution Principle (LSP)
I – Interface Segregation Principle (ISP)
D – Dependency Inversion Principle (DIP)
In this guide, we’ll break down each principle, explain its meaning, and show practical Flutter/Dart code examples that you can apply in your projects.
Table of contents:
Prerequisites
Before diving in, you should have:
Know how to use Dart and Flutter.
Basic understanding of OOP concepts (classes, inheritance, interfaces, and polymorphism).
Flutter installed on your system (Flutter installation guide).
Familiarity with running Flutter apps on an emulator or physical device.
How to Implement the Single Responsibility Principle (SRP) in Flutter
Definition: A class should have only one reason to change. This principle prevents “god classes” that try to do everything. Instead, each class should handle one specific responsibility.
Flutter Example
// Single Responsibility Principle (SRP)
// Logger class handles only logging
class Logger {
void log(String message) {
print(message);
}
}
// UserManager class handles only user management
class UserManager {
final Logger _logger;
UserManager(this._logger);
void addUser(String username) {
// Business logic for adding a user
_logger.log('User $username added.');
}
}
Code Explanation
class Logger { ... }
→ This class is responsible only for logging. It has a single methodlog
.class UserManager { ... }
→ This class manages users (for example, adding them).final Logger _logger;
→ Instead of logging directly,UserManager
depends on theLogger
class.addUser(String username)
→ Focuses on user management, not logging.
By splitting responsibilities, we can replace Logger
with another implementation (like saving logs to a file) without touching UserManager
.
SRP in real Flutter projects:
AuthService
for authentication logicApiService
for network callsDatabaseService
for local persistence
How to Implement the Open-Closed Principle (OCP) in Flutter
Definition: Classes should be open for extension but closed for modification. This means you shouldn’t need to change existing code when adding new features—just extend it.
Flutter Example
// Open/Closed Principle (OCP)
// Base abstraction
abstract class Shape {
double area();
}
// Circle class extends Shape
class Circle implements Shape {
final double radius;
Circle(this.radius);
@override
double area() => 3.14 * radius * radius;
}
// Square class extends Shape
class Square implements Shape {
final double side;
Square(this.side);
@override
double area() => side * side;
}
Code Explanation
abstract class Shape
→ Defines the contractarea()
for all shapes.class Circle implements Shape
→ Extends behavior without modifying existing code.class Square implements Shape
→ Adds another shape in the same way.
If you want to add Triangle
, you just create a new class instead of editing Shape
, Circle
, or Square
.
OCP in real Flutter projects:
Adding new UI components without modifying the base widget class.
Supporting new payment methods in an app by implementing a
PaymentMethod
interface.
How to Implement the Liskov Substitution Principle (LSP) in Flutter
Definition: Subclasses should be substitutable for their base classes without breaking functionality. If your function accepts a base type, it should also accept its subtypes without issues.
Flutter Example
// Liskov Substitution Principle (LSP)
void printArea(Shape shape) {
print('Area: ${shape.area()}');
}
void main() {
Shape circle = Circle(5);
Shape square = Square(4);
printArea(circle); // Works with Circle
printArea(square); // Works with Square
}
Code Explanation
void printArea(Shape shape)
→ Works with any class implementingShape
.circle
andsquare
→ Both are valid substitutes forShape
.
LSP in real Flutter projects:
A
TextField
can be replaced with aPasswordField
widget, as both behave like input fields.A
FirebaseAuthService
can be swapped with aMockAuthService
in tests.
How to Implement the Interface Segregation Principle (ISP) in Flutter
Definition: Clients should not depend on methods they don’t use. Instead of one big interface, split it into smaller, focused interfaces.
Flutter Example
// Interface Segregation Principle (ISP)
abstract class Flyable {
void fly();
}
abstract class Swimmable {
void swim();
}
class Bird implements Flyable {
@override
void fly() => print('Bird is flying.');
}
class Fish implements Swimmable {
@override
void swim() => print('Fish is swimming.');
}
Code Explanation
Flyable
andSwimmable
→ Separate contracts for flying and swimming.Bird implements Flyable
→ Birds don’t need aswim
method.Fish implements Swimmable
→ Fish don’t need afly
method.
ISP in real Flutter projects:
Splitting
AuthService
into smaller interfaces likeLoginService
,RegistrationService
,PasswordResetService
.Widgets implementing only the properties they actually need.
How to Implement the Dependency Inversion Principle (DIP) in Flutter
Definition: High-level modules should depend on abstractions, not concrete implementations. This makes your code more flexible and testable.
Flutter Example
// Dependency Inversion Principle (DIP)
// Abstraction
abstract class Database {
void saveData(String data);
}
// Concrete implementation
class SqlDatabase implements Database {
@override
void saveData(String data) {
print('SQL: Data saved -> $data');
}
}
// High-level module
class DataService {
final Database _database;
DataService(this._database);
void processData(String data) {
_database.saveData(data);
}
}
void main() {
Database db = SqlDatabase();
DataService service = DataService(db);
service.processData('User info');
}
Code Explanation
abstract class Database
→ Defines the contract for saving data.class SqlDatabase implements Database
→ One possible implementation.class DataService
→ Depends only on theDatabase
abstraction, notSqlDatabase
.Database db = SqlDatabase();
→ Implementation can easily be swapped (for example, withFirebaseDatabase
).
DIP in real Flutter projects:
Using
AuthRepository
instead of tying code directly to Firebase.Injecting services with
get_it
orriverpod
.
Testing and Refactoring with SOLID
Unit tests become easier since you can mock dependencies.
Refactoring is smoother because responsibilities are well-separated.
Code reviews catch SOLID violations early.
Final Thoughts
By following the SOLID principles in Flutter and Dart:
Your code becomes more maintainable.
New features are easier to add.
Testing becomes much simpler.
These principles are not just theory, they directly improve real-world Flutter projects. Start small, apply one principle at a time, and you’ll quickly see your codebase evolve into something much more scalable and future-proof.
References
Robert C. Martin – Clean Architecture