All about SOLID Principles in Flutter: Examples and Tips

8 min read

As a senior software engineer, one of the most important principles I adhere to is the SOLID principles of object-oriented programming. These principles are a set of guidelines that help developers write code that is easy to maintain, extend, and test. In this article, we’ll take a look at the SOLID principles in the context of Flutter and provide real-world examples to help you understand them better. And of course, we’ll sprinkle in a few memes to keep things interesting!

SOLID Principles Overview

SOLID is an acronym that stands for five principles of object-oriented programming:

  1. Single Responsibility Principle (SRP): A class should have only one reason to change.
  2. Open/Closed Principle (OCP): A class should be open for extension but closed for modification.
  3. Liskov Substitution Principle (LSP): Subtypes should be substitutable for their base types.
  4. Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don’t use.
  5. Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions.

Now let’s dive into each principle and see how they apply in Flutter development.

Single Responsibility Principle (SRP)

The SRP states that a class should have only one reason to change. In other words, a class should have only one responsibility. This helps to keep the code clean, organized, and easy to maintain.

Let’s take an example of a class that handles both the UI rendering and the business logic. This is not a good design because if we want to change the UI, we have to modify the business logic code as well. Instead, we can separate the UI and the business logic into different classes to follow the SRP.

Here’s an example of how to separate the UI and business logic in Flutter:

Copy
class MyHomePage extends StatefulWidget {
  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My App'),
      ),
      body: Center(
        child: Text(
          'Hello, World!',
          style: TextStyle(fontSize: 30),
        ),
      ),
    );
  }
}

class HomePageViewModel {
  String getText() => 'Hello, World!';
}

In this example, the MyHomePage widget only handles the UI rendering, while the HomePageViewModel class handles the business logic. This separation makes it easier to modify each component independently without affecting the other.

Open/Closed Principle (OCP)

The OCP states that a class should be open for extension but closed for modification. This means that we should be able to add new features or behaviors to a class without changing its existing code. This helps to maintain the existing code and avoid introducing new bugs.

Let’s take an example of a class that handles the rendering of different shapes. We can use the OCP principle to allow the addition of new shapes without modifying the existing code.

Here’s an example of how to use the OCP principle in Flutter:

Copy
abstract class Shape {
  void draw();
}

class Circle implements Shape {
  
  void draw() => print('Drawing a circle.');
}

class Rectangle implements Shape {
  
  void draw() => print('Drawing a rectangle.');
}

class ShapePainter {
  List<Shape> shapes;

  void paint(Canvas canvas) {
    shapes.forEach((shape) => shape.draw());
  }
}

In this example, we have an abstract Shape class with a draw method that will be implemented by its concrete subclasses. The ShapePainter class takes a list of shapes and paints them on the canvas by calling their draw method.

Now, let’s say we want to add a new shape, such as a triangle. We can do this without modifying the ShapePainter class by creating a new subclass of Shape:

Copy
class Triangle implements Shape {
  
  void draw() => print('Drawing a triangle.');
}

And then we can use it like this:

Copy
final shapes = [Circle(), Rectangle(), Triangle()];
final painter = ShapePainter(shapes);

This way, we can add new shapes without modifying the existing code, which makes it easier to maintain and extend the application.

Liskov Substitution Principle (LSP)

The LSP states that subtypes should be substitutable for their base types. This means that if we have a class A that is a subtype of class B, we should be able to use an object of type A wherever an object of type B is expected, without affecting the correctness of the program.

Let’s take an example of a class hierarchy that represents different types of animals. We can use the LSP to ensure that any subclass can be used interchangeably with its superclass.

Here’s an example of how to use the LSP in Flutter:

Copy
abstract class Animal {
  String name;
  String speak();
}

class Dog extends Animal {
  
  String speak() => 'Woof!';
}

class Cat extends Animal {
  
  String speak() => 'Meow!';
}

class AnimalApp {
  void run() {
    final animals = [Dog(), Cat()];
    animals.forEach((animal) => print('${animal.name} says ${animal.speak()}'));
  }
}

In this example, we have an abstract Animal class with a name property and a speak method that will be implemented by its concrete subclasses. The AnimalApp class creates a list of animals and prints out their names and the sounds they make.

Thanks to the LSP, we can treat each animal as an Animal object, even though they are actually Dog and Cat objects. This makes the code more flexible and easier to maintain.

Interface Segregation Principle (ISP)

The ISP states that clients should not be forced to depend on interfaces they don’t use. This means that we should break down large interfaces into smaller, more specialized ones, so that clients only need to implement the methods they actually use.

Let’s take an example of an interface that represents a shape. We can use the ISP to break down this interface into smaller interfaces that represent specific shape behaviors.

Here’s an example of how to use the ISP in Flutter:

Copy
abstract class Shape {
  double get area;
}

abstract class HasPerimeter {
  double get perimeter;
}

class Circle implements Shape, HasPerimeter {
  final double radius;

  Circle(this.radius);

  
  double get area => 3.14 * radius * radius;

  
  double get perimeter => 2 * 3.14 * radius;
}

class Square implements Shape, HasPerimeter {
  final double side;

  Square(this.side);

  
  double get area => side * side;

  
  double get perimeter => 4 * side;
}

In this example, we have an abstract Shape interface with a get area method, and an HasPerimeter interface with a get perimeter method. The Circle and Square classes implement both interfaces, since they have both an area and a perimeter.

By breaking down the Shape interface into smaller interfaces, we make it easier for clients to use only the methods they need. For example, if a client only needs to calculate the area of a shape, they can depend only on the Shape interface, and not on the HasPerimeter interface.

Dependency Inversion Principle (DIP)

The DIP states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. This means that we should define interfaces or abstract classes for our dependencies, so that we can easily switch between different implementations without affecting the rest of the application.

Let’s take an example of a class that sends notifications. We can use the DIP to make this class more flexible, by defining an interface for its dependencies.

Here’s an example of how to use the DIP in Flutter:

Copy
abstract class NotificationService {
  Future<void> sendNotification(String message);
}

class EmailService implements NotificationService {
  
  Future<void> sendNotification(String message) async {
    // send email notification
  }
}

class PushNotificationService implements NotificationService {
  
  Future<void> sendNotification(String message) async {
    // send push notification
  }
}

class NotificationSender {
  final NotificationService _service;

  NotificationSender(this._service);

  Future<void> send(String message) => _service.sendNotification(message);
}

In this example, we have an abstract NotificationService interface with a sendNotification method, and two concrete implementations: EmailService and PushNotificationService. The NotificationSender class depends on the NotificationService interface, instead of depending directly on either EmailService or PushNotificationService.

By depending on an interface instead of a concrete implementation, we make it easier to switch between different notification services, without having to modify the NotificationSender class.

Conclusion

The SOLID principles are an important set of guidelines for writing maintainable and flexible software. By following these principles, we can create code that is easier to understand, modify, and extend.

In this article, we’ve covered the five SOLID principles, and provided real-world examples of how to use them in Flutter. We hope this article has helped you understand the importance of these principles, and how to apply them to your own Flutter projects.

Related Blogs
Adjusting MinSdkVersion in Flutter for Android: 2 Simple Approaches [2023]

Adjusting MinSdkVersion in Flutter for Android: 2 Simple Approaches [2023]

Enhance your Flutter app's compatibility by tweaking the Android minSdkVersion. This comprehensive guide covers easy techniques for projects pre and post Flutter 2.8, guaranteeing optimal functionality.

AndroidFlutterFlutter DevelopmentMin Sdk Version

July 20, 2023

All about SOLID Principles in Flutter: Examples and Tips

All about SOLID Principles in Flutter: Examples and Tips

Check out this guide on SOLID principles in Flutter by Mihir Pipermitwala, a software engineer from Surat. Learn with real-world examples!

DartFlutterSolid Principles

April 11, 2023

Top 9 Local Databases for Flutter in 2023: A Comprehensive Comparison

Top 9 Local Databases for Flutter in 2023: A Comprehensive Comparison

Looking for the best local database for your Flutter app? Check out our comprehensive comparison of the top 9 local databases for Flutter in 2023.

CodingFlutterFlutter Local DatabasesLearn To CodeNosql

April 06, 2023

Making the Right Call: A Comparison of Flutter HTTP Clients Dio, HTTP, and GraphQL

Making the Right Call: A Comparison of Flutter HTTP Clients Dio, HTTP, and GraphQL

Explore the pros and cons of Dio, HTTP, and GraphQL as HTTP client libraries for Flutter with real-world examples and code snippets.

App ArchitectureComparisonDioFlutterFlutter Packages

April 04, 2023

Related Tutorials
A Comprehensive Guide to Flutter Buttons: Choosing the Right One for Your App

A Comprehensive Guide to Flutter Buttons: Choosing the Right One for Your App

A quick guide for you to understand and choose which Button widget suits your needs

FlutterFlutter ButtonFlutter DevelopmentText Button

April 17, 2024

How to Show Automatic Internet Connection Offline Message in Flutter

How to Show Automatic Internet Connection Offline Message in Flutter

You have to use the 'Connectivity Flutter Package' to achieve this feature on your App. This package helps to know whether your device is online or offline.

Connectivity PlusDependenciesFlutterFlutter DevelopmentFlutter Packages

April 09, 2024

Mastering TabBar and TabBarView Implementation in Flutter: A Comprehensive Guide for 2023

Mastering TabBar and TabBarView Implementation in Flutter: A Comprehensive Guide for 2023

Discover the art of implementing TabBar and TabBarView widgets in Flutter with our comprehensive guide for 2023. Learn step by step how to create captivating user interfaces, customize tab indicators, enable scrollable tabs, change tabs programmatically, and more. Elevate your Flutter app's navigation and user experience with expert insights and practical examples.

CodingDefault Tab ControllerFlutterLearn To CodeTab Controller

July 26, 2023

Enhancing File Manipulation in Flutter: Best Practices and Examples

Enhancing File Manipulation in Flutter: Best Practices and Examples

Master Flutter file manipulation like a pro. Our comprehensive blog provides insights on permission management, directory handling, and practical read-write scenarios. Elevate your app's file management.

CodingFlutterFlutter DevelopmentFlutter File OperationFlutter Path Provider

June 30, 2023

Related Recommended Services
Visual Studio Code for the Web

Visual Studio Code for the Web

Build with Visual Studio Code, anywhere, anytime, in your browser.

idevisual-studiovisual-studio-codevscodeweb
Renovate | Automated Dependency Updates

Renovate | Automated Dependency Updates

Renovate Bot keeps source code dependencies up-to-date using automated Pull Requests.

automated-dependency-updatesbundlercomposergithubgo-modules
Kubecost | Kubernetes cost monitoring and management

Kubecost | Kubernetes cost monitoring and management

Kubecost started in early 2019 as an open-source tool to give developers visibility into Kubernetes spend. We maintain a deep commitment to building and supporting dedicated solutions for the open source community.

cloudkubecostkubernetesopen-sourceself-hosted
Related Recommended Stories
Awesome Python

Awesome Python

An opinionated list of awesome Python frameworks, libraries, software and resources

awesomecollectionsgithubpythonpython-framework
Found means fixed - Introducing code scanning autofix, powered by GitHub Copilot and CodeQL

Found means fixed - Introducing code scanning autofix, powered by GitHub Copilot and CodeQL

Now in public beta for GitHub Advanced Security customers, code scanning autofix helps developers remediate more than two-thirds of supported alerts with little or no editing.

code-scanningcodeqlcodinggithubgithub-advanced-security
Awesome Java

Awesome Java

A curated list of awesome frameworks, libraries and software for the Java programming language

awesomebuildcachingclicode-analysis
Awesome iOS

Awesome iOS

A curated list of awesome iOS ecosystem, including Objective-C and Swift Projects

analyticsapp-routingapp-storeapple-swiftapple-tv