
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 haveone, 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 namedPatient
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 thePatient
class has multiple reasons to change. If you need to add a new functionality related to billing, you'd have to modify thePatient
class, potentially affecting other areas that rely on patient information management.
Here's a better approach using SRP:
classPatient{finalStringname;finalintid;finalDateTimedateOfBirth;// Other attributes related to patient informationPatient(this.name,this.id,this.dateOfBirth);}classMedicalProcedure{finalStringname;finalDateTimedate;finalPatientpatient;// Methods to perform or record medical proceduresMedicalProcedure(this.name,this.date,this.patient);}classBilling{finaldoubleamount;finalPatientpatient;finalList<MedicalProcedure>procedures;// Methods to calculate and manage billsBilling(this.amount,this.patient,this.procedures);}classAppointment{finalDateTimedateTime;finalPatientpatient;finalStringdoctorName;// Methods to manage appointmentsAppointment(this.dateTime,this.patient,this.doctorName);}
We have separate classes forPatient
,MedicalProcedure
,Billing
, andAppointment
.
Each class focuses on a specific responsibility:Patient
- Stores patient informationMedicalProcedure
- Represents a medical procedure performed on a patientBilling
- Manages bills associated with a patient and proceduresAppointment
- 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 widgetPatientDetails
that displays core patient information likename
,ID
, anddate
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 modifyingPatientDetails
. This can become cumbersome and error-prone as the app grows.
Using OCP:
Abstract Base Class (PatientDetail
):
Create an abstract base classPatientDetail
with a methodbuildDetailWidget
responsible for displaying a specific detail.
abstractclassPatientDetail{finalStringtitle;PatientDetail(this.title);WidgetbuildDetailWidget(BuildContextcontext,Patientpatient);}
Concrete Detail Widgets:
- Create concrete subclasses like
NameDetail
,IdDetail
,DateOfBirthDetail
, etc. inheriting fromPatientDetail
. - Each subclass implements
buildDetailWidget
to display its specific information.
classNameDetailextendsPatientDetail{NameDetail():super("Name");@overrideWidgetbuildDetailWidget(BuildContextcontext,Patientpatient){returnText(patient.name);}}// Similarly, create widgets for ID, DateOfBirth, etc.
PatientDetails Widget:
- The
PatientDetails
widget now holds a list ofPatientDetail
objects. - It iterates through the list and uses each detail object's
buildDetailWidget
method to build the UI.
classPatientDetailsextendsStatelessWidget{finalPatientpatient;finalList<PatientDetail>details;PatientDetails(this.patient,this.details);@overrideWidgetbuild(BuildContextcontext){returnColumn(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 ofPatientDetail
with its specific widget logic. No changes are required toPatientDetails
.
Flexibility: You can control the order of displayed details by adjusting the order in the details list ofPatientDetails
.
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 widgetMedicalHistoryTile
that displays a patient's medical history entry with generic details like date and title. You might then create a subclassAllergyHistoryTile
that inherits fromMedicalHistoryTile
but also displays information specific to allergies.
We have a base widgetMedicalHistoryTile
that displays basic information about a medical history entry. We then create subclasses for specific types of entries, likeAllergyHistoryTile
andMedicationHistoryTile
.
Base Class - medical_history_tile.dart:
abstractclassMedicalHistoryTileextendsStatelessWidget{finalStringtitle;finalDateTimedate;MedicalHistoryTile(this.title,this.date);@overrideWidgetbuild(BuildContextcontext){returnListTile(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:
classAllergyHistoryTileextendsMedicalHistoryTile{finalStringallergen;AllergyHistoryTile(super.title,super.date,this.allergen);@overrideWidgetbuild(BuildContextcontext){returnsuper.build(context);// Reuse base tile structure}WidgetgetTrailingWidget(BuildContextcontext){returnText("Allergen:$allergen",style:TextStyle(color:Colors.red),);}}
AllergyHistoryTile
inherits fromMedicalHistoryTile
and adds an allergen property. It reuses the base class'sbuild
method for the core structure and introduces a new methodgetTrailingWidget
to display allergy-specific information (red color for emphasis).
Subclass - medication_history_tile.dart:
classMedicationHistoryTileextendsMedicalHistoryTile{finalStringmedication;finalintdosage;MedicationHistoryTile(super.title,super.date,this.medication,this.dosage);@overrideWidgetbuild(BuildContextcontext){returnListTile(title:Text(title),subtitle:Text(DateFormat.yMMMd().format(date)),trailing:Text("Medication:$medication ($dosage mg)",),);}}
MedicationHistoryTile
inherits from the base class and addsmedication
anddosage
properties. It overrides the entirebuild
method to include medication details in the trailing widget, demonstrating a different approach compared toAllergyHistoryTile
.
Usage and LSP Compliance:
Now, consider using these widgets in your app:
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){finalentry=medicalHistory[index];returnentry.build(context);// Polymorphism in action},);
This code iterates through a list ofMedicalHistoryTile
objects (including subclasses). Because both subclasses adhere to the LSP principle, you can usebuild(context)
on any of them, and it will work as expected. TheAllergyHistoryTile
will display its red trailing widget, whileMedicationHistoryTile
will use its customListTile
withmedication
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 asMedicalHistoryTile
objects for building the list view, and the appropriatebuild
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 interfaceHospitalService
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 entireHospitalService
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.
abstractclassPatientInfoService{Future<Patient>getPatient(intpatientId);}abstractclassMedicalProcedureService{Future<void>performProcedure(StringprocedureName,Patientpatient);}abstractclassAppointmentService{Future<Appointment>scheduleAppointment(DateTimedateTime,Patientpatient,StringdoctorName);}abstractclassBillingService{Future<Bill>generateBill(Patientpatient,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.
classPatientInfoServiceImplimplementsPatientInfoService{// Implementation for fetching patient information}classDisplayPatientInfoextendsStatelessWidget{finalintpatientId;DisplayPatientInfo(this.patientId);@overrideWidgetbuild(BuildContextcontext){returnFutureBuilder<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 aPatientDetails
widget that directly fetches patient information from aPatientRepository
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.
abstractclassPatientDataProvider{Future<Patient>getPatient(intpatientId);}
2. Concrete Implementations:
- Create concrete classes like
NetworkPatientRepository
andLocalPatientRepository
implementingPatientDataProvider
. These handle fetching data from the network or local storage.
classNetworkPatientRepositoryimplementsPatientDataProvider{// Implementation for fetching patient information from network}classLocalPatientRepositoryimplementsPatientDataProvider{// Implementation for fetching patient information from local storage (optional)}
3.Dependency Injection:
- Inject the
PatientDataProvider
dependency into thePatientDetails
widget constructor.
Use a dependency injection framework (likeProvider orBLoC) for managing dependencies in your app.
classPatientDetailsextendsStatelessWidget{finalintpatientId;finalPatientDataProviderpatientDataProvider;PatientDetails(this.patientId,this.patientDataProvider);@overrideWidgetbuild(BuildContextcontext){returnFutureBuilder<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., useNetworkPatientRepository
orLocalPatientRepository
) 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!
Top comments(2)

Great overview on SOLID principles! Applying these in Flutter is essential for building organized and maintainable apps. Looking forward to implementing these strategies in my projects. Thanks for sharing! 🚀

- LocationIN
- Joined
Hey, Thanks! I'm glad you found it helpful! 🙏
For further actions, you may consider blocking this person and/orreporting abuse