S.O.L.I.D Principle in Dart Flutter
S.O.L.I.D Principle in Dart Flutter

Hello, dart and flutter developers! Today’s topic is SOLID principles in Dart. This is not about adding new language features; rather, it is about improving code quality and maintainability. These best practices are intended to be used in any object-oriented language, not just Dart.

Dart SOLID

What are the fundamental principles of Flutter?

The acronym SOLID stands for five well-known design principles:

  • Single Responsibility.
  • Open-Closed.
  • Liskov Substitution.
  • Interface Segregation.
  • and Dependency Inversion.

All of which will be discussed further below. They are very popular among developers and are widely accepted as good practices.

What issue did these S.O.L.I.D principles solve?

  •  Jumping multiple methods with the same name and spending a significant amount of time to fix a minor bug.
  • Rereading the code several times to find the section that needs to be changed.

It’s difficult to understand what the method does.

Essentially, we spend far more time understanding than we do writing code.


SOLID Principal in dart – flutter

Single Responsibility principle

This principle states that a class should only change for one reason. In other words, you should design classes with a single “responsibility” to make them easier to maintain and more difficult to break.

As the name implies, it is solely responsible. A class should only be responsible for one thing, which means it should only change for one reason.

Let’s start with a bad practice in which maintenance is unpleasant. Because everything is in one place: validation, getRequest, and painting.

class Shapes {
  List < String > cache = List < > ();

  // Calculations
  double squareArea(double l) {
    /* ... */ }
  double circleArea(double r) {
    /* ... */ }
  double triangleArea(double b, double h) {
    /* ... */ }

  // Paint to the screen
  void paintSquare(Canvas c) {
    /* ... */ }
  void paintCircle(Canvas c) {
    /* ... */ }
  void paintTriangle(Canvas c) {
    /* ... */ }

  // GET requests
  String wikiArticle(String figure) {
    /* ... */ }
  void _cacheElements(String text) {
    /* ... */ }
}

Because it handles internet requests, work, and calculations all in one place, This class completely destroys the SRP. This class will change frequently in the future: whenever a method requires maintenance, you’ll have to change some code, which may break other parts of the class. What do you think about this?

// Calculations and logic
abstract class Shape {
  double area();
}

class Square extends Shape {}
class Circle extends Shape {}
class Rectangle extends Shape {}

// UI painting
class ShapePainter {
  void paintSquare(Canvas c) {
    /* ... */ }
  void paintCircle(Canvas c) {
    /* ... */ }
  void paintTriangle(Canvas c) {
    /* ... */ }
}

// Networking
class ShapesOnline {
  String wikiArticle(String figure) {
    /* ... */ }
  void _cacheElements(String text) {
    /* ... */ }
}

Open-Closed Principle

You should be able to extend the behavior of a (class, module, function, etc.) without modifying it.

According to the open-closed principle, a good programmer should add new behaviors without changing the existing source code. This concept is notoriously described as “classes should be open for extension and closed for changes”.

// Looks good but wait! Customer wants a revision in the last second and wants a third number to add. How we can solve this problem without breaking the app?int sum(int a, int b) {
int sum(int a, int b) {
  return a + b;
}

// Breaks old functionality
int sum(int a, int b, int c) {
  return a + b + c;
}

// There is no problem for old codes because this one wont be affected by new codes
int sum(int a, int b, [int c = 0]) {
  return a + b + c;
}


Liskov Substitution Principle

This principle suggests that “parent classes should be easily substituted with their child classes without blowing up the application”. To explain this, consider the following example.

The architecture ensures that the subclass maintains the logic correctness of the code. Composition with interfaces is preferred over inheritance.

abstract class StorageService {
  void get(String key);
  void put(String key, String value);
}

class HiveService implements StorageService {
  @override
  void get(String path) {}

  @override
  void put(String key, String value) {}
}

class ObjectBoxService implements StorageService {
  @override
  void get(String key) {}

  @override
  void put(String key, String value) {}
}

Interface Segregation Principle

This principle suggests that “many client specific interfaces are better than one general interface“. This is the first principle that applies to an interface; the other three principles apply to classes.

abstract class Teacher{
  void work();
  void sleep();
}

class Human implements Teacher{
  void work() => print("I do a lot of work");
  void sleep() => print("I need 10 hours per night...");
}

class Robot implements Teacher{
  void work() => print("I always work");
  void sleep() {} // ??
}

The issue here is that robots do not require sleep, so we are unsure how to implement that method. Worker represents an entity capable of performing work, and it is assumed that if you can work, you can also sleep. In our case, this assumption is not always correct, so we’ll divide the class:

abstract class Teacher{
  void work();
}

abstract class Sleeper {
  void sleep();
}

class Human implements Teacher, Sleeper {
  void work() => print("I do a lot of work");
  void sleep() => print("I need 10 hours per night...");
}

class Robot implements Teacher{
  void work() => print("I'm always iiii work as teacher");
}

Dependency Inversion Principle (DIP)

The issue here is that robots do not require sleep, so we are unsure how to implement that method. Worker represents an entity capable of performing work, and it is assumed that if you can work, you can also sleep. In our case, this assumption is not always correct, so we’ll divide the class:

Assume we have a Firebase-integrated app and want to migrate the database to a PhpDataBaseserver later. If we make them rely on an interface, there will be no problem; all we need to do is change the service.

As if you were changing the batteries in a toy. The toy is still an old toy, but the batteries are new.

abstract class DataApi {
  Future get(String path);
}

class FirebaseService implements DataApi {
  @override
  Future get(String path) async {
    return await Future.value('Data from Firebase');
  }
}

class PhpDBService implements DataApi {
  @override
  Future get(String path) async {
    return await Future.value('Data from PhpDBService ');
  }
}

Instead of use

abstract class DataService {
  FutureOr<String> fetch();
}
class FirebaseService extends DataService {
  @override
  Future<String> fetch() async => await 'data';
}
class LocaDataService extends DataService {
  @override
  String fetch() => 'data';
}

Conclusion

Avoid being trapped by Solid.

S.O.L.I.D. Principles are not rules, but rather principles.

When using this, always use common sense. Your goal is to make your code maintainable and easy to extend.

Avoid fragmenting your code excessively for the sake of SRP or S.O.L.I.D.

Thank you for reading the SOLID principles ….Have a fine day!!