Welcome back to the second part of our journey into mastering the SOLID principles in Flutter. In the previous part, we explored:
β
S β Single Responsibility Principle
β
O β Open/Closed Principle
Today, weβre diving into two more important concepts thatβll help make your code more safe, scalable, and focused:
L - Liskov Substitution Principle (LSP)
π Definition:
Subtypes must be substitutable for their base types without altering the correctness of the program.
ποΈ Real-Life Analogy:
You rent a sports car, and later they replace it with a sedan.
You expect:
- It should still drive π
- Use the same controls πΉοΈ
- Not break down when you turn the key π
Thatβs LSP β if something claims to be a type, it should behave like that type without surprises.
π‘ Flutter-Specific Example:
β Wrong: Violating LSP
class Bird {
void fly() {
print("Flying...");
}
}
class Ostrich extends Bird {
@override
void fly() {
throw Exception("Ostriches can't fly!");
}
}
Problem:
- Ostrich is a Bird, but replacing a Bird with an Ostrich breaks the app!
- It violates expectations of the base class.
β
Right: Respect LSP via abstraction
abstract class Bird {}
abstract class FlyingBird extends Bird {
void fly();
}
class Sparrow extends FlyingBird {
@override
void fly() {
print("Flying high!");
}
}
class Ostrich extends Bird {
// Doesnβt extend FlyingBird, so no broken promises
}
Now:
- Code expecting a FlyingBird will only get actual flying birds.
- Ostrich wonβt be misused in flying-related logic.
π¦ Flutter Real Case β Widget Inheritance
Avoid this π
class CustomButton extends ElevatedButton {
// Trying to override core ElevatedButton behavior
// Can break consumers expecting regular ElevatedButton
}
Instead: β
class CustomButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
const CustomButton({required this.label, required this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
child: Text(label),
);
}
}
Why? Youβre creating new behavior without violating the contract of built-in widgets.
π Key Takeaways:
- Donβt override methods in a way that breaks expectations.
- Subclass only when the child truly behaves like the parent.
- If behavior differs, prefer composition or separation, not inheritance.
π§ Interview One-Liner:
βLSP means I can swap any child class with its parent and the app still works correctly. In Flutter, I use it by avoiding inappropriate overrides and designing my inheritance carefully.β
I - Interface Segregation Principle (ISP)
π Definition:
Clients should not be forced to depend on interfaces they do not use.
π± Real-Life Analogy:
Imagine a food delivery app asking you to:
- Select your food β
- Enter address β
- Rate the driver β
- But also... provide restaurant inventory info? β
Why should you, the end-user, be responsible for restaurant tasks?
Similarly, in code:
- Donβt force a class to implement methods it doesnβt need.
π‘ Flutter-Specific Example:
β Wrong:
abstract class SocialFeatures {
void post();
void like();
void comment();
void blockUser();
}
class ReadOnlyUser implements SocialFeatures {
@override
void post() {
throw UnimplementedError();
}
@override
void like() {
throw UnimplementedError();
}
@override
void comment() {
throw UnimplementedError();
}
@override
void blockUser() {
print("User blocked");
}
}
Problem:
ReadOnlyUser is forced to implement methods it doesnβt need.
β
Right:
Break interfaces into smaller ones:
abstract class CanBlock {
void blockUser();
}
abstract class CanPost {
void post();
void like();
void comment();
}
class ReadOnlyUser implements CanBlock {
@override
void blockUser() {
print("User blocked");
}
}
class ActiveUser implements CanPost, CanBlock {
@override
void post() => print("Posted");
@override
void like() => print("Liked");
@override
void comment() => print("Commented");
@override
void blockUser() => print("Blocked");
}
Now:
Each class only implements what it needs, and nothing else.
π¦ Flutter Example β Bad Form Validation
abstract class FormValidator {
String? validateEmail();
String? validatePhone();
String? validatePassword();
}
If your form only has email, you're still forced to implement all three.
β
Split Validators:
abstract class EmailValidator {
String? validateEmail();
}
abstract class PasswordValidator {
String? validatePassword();
}
class LoginValidator implements EmailValidator, PasswordValidator {
@override
String? validateEmail() => null;
@override
String? validatePassword() => null;
}
π Key Takeaways:
- Prefer many small, specific interfaces over one large interface.
- Avoid forcing unrelated responsibilities onto a class.
- Makes code cleaner and more flexible.
π§ Interview One-Liner:
βInterface Segregation helps me design focused contracts. In Flutter, I split large interfaces into smaller validators or roles so that classes only take what they need β nothing extra.β
β
That wraps up Part 2 of the series!
Today, we explored:
π© Liskov Substitution Principle β Why subclasses should behave like their parents
π¦ Interface Segregation Principle β How to keep interfaces lean and focused
With real-life analogies and clean Flutter examples, you now know how to avoid broken inheritance and bloated interfaces β two of the most common clean-code traps in app development.
π¬ If this helped, Iβd love to hear your feedback, thoughts, or questions in the comments.
π Stay tuned for Part 3, where weβll wrap up the series with the final (and powerful) principle:
π« D β Dependency Inversion Principle
Letβs finish strong and make our Flutter code truly solid. π₯
Top comments (0)