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)