Mastering SOLID principles in Flutter
Tags: development, principles, flutter, mobile
Published: 2024-05-19 17:33:28

---
title: Mastering SOLID principles in Flutter
published: true
description: 
tags: #development #principles #flutter #mobile
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lmttli422fubw9mo274y.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-05-13 12:44 +0000
---

SOLID principles are a set of guidelines for writing clean, maintainable, and scalable object-oriented code. By applying these principles to your Flutter development, you can create well-structured applications that are easier to understand, modify, and extend in the future. Here's a breakdown of each principle of Flutter development:

**1. Single Responsibility Principle (SRP):**
The Single Responsibility Principle (SRP) is one of the fundamental principles of object-oriented programming within the SOLID principles. It states that a class should have **one, and only one, reason to change.**

In simpler terms, a class should be focused on a single functionality and avoid becoming a cluttered mess trying to do too many things. This promotes better code organization, maintainability, and testability.

Let's consider a scenario in a hospital management system. Traditionally, you might have a class named `Patient` that handles various functionalities like:

* Storing patient information (name, ID, etc.)
* Performing medical procedures
* Generating bills
* Managing appointments

 Following SRP, this approach becomes problematic because the `Patient` class has multiple reasons to change. If you need to add a new functionality related to billing, you'd have to modify the `Patient` class, potentially affecting other areas that rely on patient information management.

 Here's a better approach using SRP:

```dart
class Patient {
  final String name;
  final int id;
  final DateTime dateOfBirth;

  // Other attributes related to patient information

  Patient(this.name, this.id, this.dateOfBirth);
}

class MedicalProcedure {
  final String name;
  final DateTime date;
  final Patient patient;

  // Methods to perform or record medical procedures

  MedicalProcedure(this.name, this.date, this.patient);
}

class Billing {
  final double amount;
  final Patient patient;
  final List<MedicalProcedure> procedures;

  // Methods to calculate and manage bills

  Billing(this.amount, this.patient, this.procedures);
}

class Appointment {
  final DateTime dateTime;
  final Patient patient;
  final String doctorName;

  // Methods to manage appointments

  Appointment(this.dateTime, this.patient, this.doctorName);
}
```


We have separate classes for `Patient`, `MedicalProcedure`, `Billing`, and `Appointment`.
Each class focuses on a specific responsibility:
`Patient` - Stores patient information
`MedicalProcedure` - Represents a medical procedure performed on a patient
`Billing` - Manages bills associated with a patient and procedures
`Appointment` - Schedules appointments for patients


**2. Open/Closed Principle (OCP):**
The Open/Closed Principle (OCP) is a fundamental principle in object-oriented programming that emphasizes extending functionality without modifying existing code. 

Here's how we apply in our App:

Our app has a base widget `PatientDetails` that displays core patient information like `name`, `ID`, and `date` of birth. Traditionally, you might add logic for displaying additional details like diagnosis or medication history directly within this widget.

However, this approach violates OCP because adding new details requires modifying `PatientDetails`. This can become cumbersome and error-prone as the app grows.

Using OCP:

**Abstract Base Class (`PatientDetail`):**
Create an abstract base class `PatientDetail` with a method `buildDetailWidget` responsible for displaying a specific detail.

```dart
abstract class PatientDetail {
  final String title;

  PatientDetail(this.title);

  Widget buildDetailWidget(BuildContext context, Patient patient);
}
```

**Concrete Detail Widgets:**
* Create concrete subclasses like `NameDetail`, `IdDetail`, `DateOfBirthDetail`, etc., inheriting from `PatientDetail`. * Each subclass implements `buildDetailWidget` to display its specific information.

```dart
class NameDetail extends PatientDetail {
  NameDetail() : super("Name");

  @override
  Widget buildDetailWidget(BuildContext context, Patient patient) {
    return Text(patient.name);
  }
}

// Similarly, create widgets for ID, DateOfBirth, etc.
```

**PatientDetails Widget:**
* The `PatientDetails` widget now holds a list of `PatientDetail` objects.
* It iterates through the list and uses each detail object's `buildDetailWidget` method to build the UI.

```dart
class PatientDetails extends StatelessWidget {
  final Patient patient;
  final List<PatientDetail> details;

  PatientDetails(this.patient, this.details);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: details.map((detail) => detail.buildDetailWidget(context, patient)).toList(),
    );
  }
}
```


**Benefits of OCP approach:**

**Easy Extension:** Adding new details like diagnosis or medication history involves creating a new concrete subclass of PatientDetail with its specific widget logic. No changes are required to PatientDetails.
**Flexibility:** You can control the order of displayed details by adjusting the order in the details list of `PatientDetails`.
**Maintainability:** Code is cleaner and easier to understand as each detail has a dedicated widget responsible for its presentation.

This is a simplified example. In a real app, you might use a state management solution (like Provider or BLoC) to manage patient data and dynamically update the list of details displayed.

**2.Liskov Substitution Principle (LSP)**

The Liskov Substitution Principle (LSP) is another cornerstone of SOLID principles. It states that subtypes should be substitutable for their base types without altering the program's correctness. In simpler terms, if you have a base class (or widget in Flutter) and derived classes (subclasses or child widgets), using a derived class wherever you expect the base class should work seamlessly without causing unexpected behavior.

**LSP in our App:**

Imagine we have a base widget MedicalHistoryTile that displays a patient's medical history entry with generic details like date and title. You might then create a subclass AllergyHistoryTile that inherits from MedicalHistoryTile but also displays information specific to allergies.


We have a base widget `MedicalHistoryTile` that displays basic information about a medical history entry. We then create subclasses for specific types of entries, like `AllergyHistoryTile` and `MedicationHistoryTile`.

 **Base Class - medical_history_tile.dart:**

```dart
abstract class MedicalHistoryTile extends StatelessWidget {
  final String title;
  final DateTime date;

  MedicalHistoryTile(this.title, this.date);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(title),
      subtitle: Text(DateFormat.yMMMd().format(date)),
    );
  }
}
```
This base class defines the core structure of a medical history tile with title and date. It's abstract because it doesn't implement the build method completely, allowing subclasses to customize the content.

**Subclass - allergy_history_tile.dart:**

```dart
class AllergyHistoryTile extends MedicalHistoryTile {
  final String allergen;

  AllergyHistoryTile(super.title, super.date, this.allergen);

  @override
  Widget build(BuildContext context) {
    return super.build(context); // Reuse base tile structure
  }

  Widget getTrailingWidget(BuildContext context) {
    return Text(
      "Allergen: $allergen",
      style: TextStyle(color: Colors.red),
    );
  }
}
```

`AllergyHistoryTile` inherits from `MedicalHistoryTile` and adds an allergen property. It reuses the base class's `build`  method for the core structure and introduces a new method `getTrailingWidget` to display allergy-specific information (red color for emphasis).

**Subclass - medication_history_tile.dart:**

```dart
class MedicationHistoryTile extends MedicalHistoryTile {
  final String medication;
  final int dosage;

  MedicationHistoryTile(super.title, super.date, this.medication, this.dosage);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(title),
      subtitle: Text(DateFormat.yMMMd().format(date)),
      trailing: Text(
        "Medication: $medication ($dosage mg)",
      ),
    );
  }
}
```

`MedicationHistoryTile` inherits from the base class and adds `medication` and `dosage` properties. It overrides the entire `build` method to include medication details in the trailing widget, demonstrating a different approach compared to `AllergyHistoryTile`.

 **Usage and LSP Compliance:**

 Now, consider using these widgets in your app:

```dart
List<MedicalHistoryTile> medicalHistory = [
  AllergyHistoryTile("Allergy to Penicillin", DateTime.now(), "Penicillin"),
  MedicationHistoryTile("Blood Pressure Medication", DateTime.now().subtract(Duration(days: 30)), "Atenolol", 50),
  MedicalHistoryTile("General Checkup", DateTime.now().subtract(Duration(days: 100))),
];

ListView.builder(
  itemCount: medicalHistory.length,
  itemBuilder: (context, index) {
    final entry = medicalHistory[index];
    return entry.build(context); // Polymorphism in action
  },
);
```

This code iterates through a list of `MedicalHistoryTile` objects (including subclasses). Because both subclasses adhere to the LSP principle, you can use `build(context)` on any of them, and it will work as expected. The `AllergyHistoryTile` will display its red trailing widget, while `MedicationHistoryTile` will use its custom `ListTile` with `medication` details.


**Benefits of LSP Approach:**

**Consistent Base Behavior:** Both subclasses reuse the base class functionality for the core tile structure.
**Subclasses Add Specialization:** Each subclass adds its specific information (allergen or medication details) without modifying the base class behavior.
**Polymorphic Usage:** You can treat all instances as `MedicalHistoryTile` objects for building the list view, and the appropriate `build` method is called based on the actual subclass type.


LSP is not just about methods. It also applies to properties and overall functionality. Subclasses should extend or specialize the behavior of the base class, not deviate from its core purpose.

**4.The Interface Segregation Principle (ISP)**

The Interface Segregation Principle (ISP) states that clients (widgets in Flutter) should not be forced to depend on methods they do not use. This principle promotes creating smaller, more specific interfaces instead of large, general-purpose ones.

One such scenario in our app where we have a single interface `HospitalService` with methods for:

* Getting patient information
* Performing medical procedures
* Scheduling appointments
* Generating bills

This approach violates ISP because a widget responsible for displaying patient information might not need functionalities like scheduling appointments or generating bills. Yet, it would still be forced to depend on the entire `HospitalService` interface.

**Solution using ISP:**

**Define Specific Interfaces:**
**1. Create separate interfaces for each functionality:**
  * `PatientInfoService` for patient information retrieval.
  * `MedicalProcedureService` for performing procedures.
  * `AppointmentService` for scheduling appointments.
  * `BillingService` for generating bills.

```dart
abstract class PatientInfoService {
  Future<Patient> getPatient(int patientId);
}

abstract class MedicalProcedureService {
  Future<void> performProcedure(String procedureName, Patient patient);
}

abstract class AppointmentService {
  Future<Appointment> scheduleAppointment(DateTime dateTime, Patient patient, String doctorName);
}

abstract class BillingService {
  Future<Bill> generateBill(Patient patient, List<MedicalProcedure> procedures);
}
```
**2. Implementations and Usage:**
 * Implement these interfaces in concrete classes that handle the actual functionalities.
 * Widgets can then depend on the specific interface they need.


```dart
class PatientInfoServiceImpl implements PatientInfoService {
  // Implementation for fetching patient information
}

class DisplayPatientInfo extends StatelessWidget {
  final int patientId;

  DisplayPatientInfo(this.patientId);

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Patient>(
      future: PatientInfoServiceImpl().getPatient(patientId),
      builder: (context, snapshot) {
        // Display patient information
      },
    );
  }
}
```

**Benefits of LSP approach**

**Improved Code Maintainability:** Smaller interfaces are easier to understand and modify.
**Reduced Coupling:** Widgets only depend on the functionalities they need.
**Enhanced Flexibility:** Easier to add new functionalities with new interfaces.
**Testability:** Easier to unit test specific functionalities through their interfaces.



**5.The Dependency Inversion Principle (DIP)** 
The Dependency Inversion Principle (DIP) states that high-level modules (widgets in Flutter) should not depend on low-level modules (concrete implementations). Both should depend on abstractions (interfaces or abstract classes). This principle encourages loose coupling and improves the testability and maintainability of your code.

 **Understanding DIP:**

The hospital app has a `PatientDetails` widget that directly fetches patient information from a `PatientRepository` class using network calls. This creates tight coupling between the widget and the repository implementation.

**Challenges with Tight Coupling:**

**Testing:** Testing the PatientDetails widget becomes difficult because you need to mock the network calls or the entire PatientRepository.
**Changes:** If you need to change the data source (e.g., from a local database to a remote API), you would need to modify the PatientDetails widget logic.

**Solution using DIP:**

**Introduce Abstraction:**
* Create an interface `PatientDataProvider` that defines methods for fetching patient information.

```dart
abstract class PatientDataProvider {
  Future<Patient> getPatient(int patientId);
}
```

**2. Concrete Implementations:**
* Create concrete classes like `NetworkPatientRepository` and `LocalPatientRepository` implementing `PatientDataProvider`. These handle fetching data from the network or local storage.


```dart
class NetworkPatientRepository implements PatientDataProvider {
  // Implementation for fetching patient information from network
}

class LocalPatientRepository implements PatientDataProvider {
  // Implementation for fetching patient information from local storage (optional)
}
```

**3.Dependency Injection:**
* Inject the `PatientDataProvider` dependency into the `PatientDetails` widget constructor.

Use a dependency injection framework (like [Provider](https://pub.dev/packages/provider) or [BLoC](https://pub.dev/packages/bloc)) for managing dependencies in your app.

```dart
class PatientDetails extends StatelessWidget {
  final int patientId;
  final PatientDataProvider patientDataProvider;

  PatientDetails(this.patientId, this.patientDataProvider);

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Patient>(
      future: patientDataProvider.getPatient(patientId),
      builder: (context, snapshot) {
        // Display patient information
      },
    );
  }
}
```

**Benefits of DIP approach**

**Loose Coupling:** Widgets are not tied to specific implementations. You can easily swap out the data source (e.g., use `NetworkPatientRepository` or `LocalPatientRepository`) without modifying the widget logic.
**Improved Testability:** You can easily mock the PatientDataProvider interface during unit tests for the PatientDetails widget.
**Increased Maintainability:** The code becomes more modular and easier to modify in the future.



>While we explored the SOLID Principle, there's always more to learn 🧑🏻‍💻. 

>What are your biggest challenges with SOLID? Share your thoughts in the comments!