SOLID Principles with Flutter

SOLID Principles with Flutter

Table of contents

No heading

No headings in the article.

SOLID principles are a set of five design principles that aim to create software that is easy to maintain, extend, and modify. These principles were introduced by Robert C. Martin (also known as "Uncle Bob") and have been widely adopted in the software development industry.

Why use SOLID principles in Flutter?

Using SOLID principles in Flutter can lead to better code quality, maintainability, and scalability of your application. By following SOLID principles, you can create code that is easier to read, understand, and modify, which can save time and effort in the long run.

Why are SOLID principles accepted?

SOLID principles provide a set of guidelines that can help developers create software that is more maintainable, extendable, and adaptable to changing requirements. Here are a few reasons why SOLID principles are accepted in the software industry:

  1. Flexibility: By adhering to SOLID principles, the software can be more flexible to change. Changes to one part of the software should not require changes to other parts, reducing the risk of introducing bugs or unintended consequences.

  2. Readability: SOLID principles promote readable code by breaking complex code into smaller, more manageable pieces, making it easier to understand and modify.

  3. Testability: SOLID principles help developers create more testable code. By breaking complex code into smaller, more focused pieces, developers can more easily test individual pieces of code in isolation.

  4. Maintainability: SOLID principles promote maintainable code by reducing the risk of introducing bugs or unintended consequences when making changes to the code.

  5. Scalability: SOLID principles promote scalable code by enabling developers to easily extend or modify the software without introducing breaking changes.

Let's have a look at how SOLID principles are useful in Flutter in detail.

S - Single Responsibility Principle: The Single Responsibility Principle (SRP) states that a class should have only one reason to change, meaning it should only be responsible for one specific task or functionality. This principle promotes code modularity, making it easier to maintain and test.

In Flutter, SRP can be applied by breaking down complex widgets into smaller, more focused widgets that have only one responsibility. For example, instead of creating a single widget that handles both the UI and the business logic, you can create separate widgets that handle each responsibility separately.

Suppose we have a Flutter app that needs to display user information. We can create a class named User which is responsible for only storing and providing user information, while another class named UserUI can be responsible for displaying user information in the UI. This way, if there are any changes needed in the user interface, we can modify only the UserUI class, without affecting the User class.

class User {
  String name;
  int age;

  User({required this.name, required this.age});

  // methods for user data retrieval and modification
}

class UserUI {
  // methods for displaying user data in the UI
}

O - Open-Closed Principle: The Open-Closed Principle (OCP) states that a class should be open for extension but closed for modification. This means that you should be able to add new functionality to a class without changing its existing code. The OCP promotes code reusability and maintainability.

In Flutter, OCP can be applied by creating interfaces or abstract classes that can be extended by other classes. For example, you can create an abstract class that defines the common properties and methods of a widget, and then create subclasses that implement the specific behaviour.

Suppose we have a class named Shape that represents a geometric shape. We can create new classes that inherit from Shape, such as Rectangle and Circle, to add new functionality specific to those shapes without modifying the existing code in Shape. For instance, we can add methods like getArea() or getPerimeter() in the subclasses, and they can be used without modifying the Shape class.

abstract class Shape {
  double getArea();
}

class Rectangle extends Shape {
  double width;
  double height;

  Rectangle({required this.width, required this.height});

  @override
  double getArea() {
    return width * height;
  }
}

class Circle extends Shape {
  double radius;

  Circle({required this.radius});

  @override
  double getArea() {
    return pi * pow(radius, 2);
  }
}

L - Liskov Substitution Principle: The Liskov Substitution Principle (LSP) states that subtypes must be substitutable for their base types. In other words, any instance of a subtype should be able to replace an instance of its base type without affecting the correctness of the program. The LSP promotes code interoperability and robustness.

In Flutter, LSP can be applied by ensuring that any subclass of a widget can be used interchangeably with the parent class. This means that any method or property of the parent class should still work when called on the subclass.

Suppose we have a class named Animal with a method makeSound(). We can create a subclass named Dog that overrides the makeSound() method to return a bark. If we use a Dog instance in place of an Animal instance, the makeSound() method should still work correctly.

class Animal {
  void makeSound() {
    print('Generic animal sound');
  }
}

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

void main() {
  Animal animal = Dog();
  animal.makeSound(); // outputs "Bark!"
}

I - Interface Segregation Principle: The Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they do not use. In other words, interfaces should be specific to the functionality they provide. The ISP promotes code cohesion and flexibility.

In Flutter, ISP can be applied by breaking down large interfaces into smaller, more focused interfaces. For example, instead of having a single interface that defines all the methods and properties for a widget, you can create smaller interfaces that define the specific behaviour.

Suppose we have an interface named Vehicle that has methods such as start() and stop(). We can create separate interfaces, such as Car and Bike, that contain only the methods specific to each type of vehicle. This way, clients can depend only on the interfaces they need, rather than a larger interface that may contain methods they don't use.

abstract class Vehicle {
  void start();
  void stop();
}

abstract class Car extends Vehicle {
  void drive();
}

abstract class Bike extends Vehicle {
  void pedal();
}

class SportsCar extends Car {
  @override
  void start() {
    print('Starting SportsCar...');
  }

  @override
  void stop() {
    print('Stopping SportsCar...');
  }

  @override
  void drive() {
    print('Driving SportsCar...');
  }
}

class MountainBike extends Bike {
  @override
  void start() {
    print('Starting MountainBike...');
  }

  @override
  void stop() {
    print('Stopping MountainBike...');
  }

  @override
  void pedal() {
    print('Pedaling MountainBike...');
  }
}

D - Dependency Inversion Principle: The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. This promotes code decoupling and flexibility.

In Flutter, DIP can be applied by using dependency injection to separate the creation of objects from their use. For example, instead of creating a widget directly in the code, you can inject the widget as a dependency from another class.

Suppose we have a class named Database that is responsible for accessing a database. We can create an interface named IDatabase and have Database implements that interface. We can then have a higher-level class, such as a Repository, depends on IDatabase rather than Database, promoting decoupling and making it easier to swap out the implementation of the database without affecting the Repository class.

abstract class IDatabase {
  Future<List<Map<String, dynamic>>>> fetchUsers();
}

class Database implements IDatabase {
  @override
  Future<List<Map<String, dynamic>>>> fetchUsers() async {
    // code to fetch users from a database
  }
}

class UserRepository {
  final IDatabase database;

  UserRepository(this.database);

  Future<List<Map<String, dynamic>>>> getUsers() async {
    return await database.fetchUsers();
  }
}

Overall, using SOLID principles in Flutter can help you create code that is easier to read, maintain, and scale, which can improve the quality and success of your application. SOLID principles provide a set of guidelines that help developers create an application that is more adaptable, maintainable and scalable, which can lead to a more successful and long-lasting software product.

💙💙 If I am wrong or do need to update any of this explanation. Please guide me through, I would love to make changes 💙💙

Keep on clapping till your claps sum to 10.

Check my previous series on Flutter interview questions for every level. Have a look into it and do share your thoughts.
https://mukundjogi.hashnode.dev/series/flutter-interview

Keep sharing🫰🏻keep learning 🙌🏻 Do practice these examples🤘🏻