The SOLID Principles in Mobile App Development: Building Robust and Maintainable Apps (Flutter)

Mobterest Studio
12 min readJun 5, 2023

--

Mobile app development is a rapidly evolving field, with new technologies and frameworks emerging frequently. Amidst this dynamic landscape, software developers face the challenge of building robust and maintainable apps that can adapt to changing requirements. To address these challenges, the SOLID principles provide a set of guiding principles for writing clean, modular, and extensible code. In this article, we will explore the SOLID principles and their significance in mobile app development.

Single Responsibility Principle (SRP)

The SRP states that a class or module should have only one reason to change. In mobile app development, this principle emphasizes breaking down complex functionalities into smaller, cohesive units. By assigning each class a single responsibility, it becomes easier to understand, test, and maintain the codebase. For example, separating user authentication and data persistence into distinct classes ensures clear separation of concerns.

Here’s an example that demonstrates the Single Responsibility Principle (SRP) in a Flutter to-do app. We’ll separate the code into two classes, each with a single responsibility: one for managing the to-do items and another for handling the UI.

// to_do_item.dart

class ToDoItem {
String title;
bool isCompleted;

ToDoItem(this.title, {this.isCompleted = false});
}

// to_do_manager.dart

class ToDoManager {
List<ToDoItem> _toDoItems = [];

void addNewItem(String title) {
final newItem = ToDoItem(title);
_toDoItems.add(newItem);
}

void markItemAsCompleted(int index) {
if (index >= 0 && index < _toDoItems.length) {
_toDoItems[index].isCompleted = true;
}
}

void deleteItem(int index) {
if (index >= 0 && index < _toDoItems.length) {
_toDoItems.removeAt(index);
}
}

List<ToDoItem> get toDoItems => _toDoItems;
}

// main.dart

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
final ToDoManager toDoManager = ToDoManager();

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ToDoScreen(toDoManager: toDoManager),
);
}
}

class ToDoScreen extends StatelessWidget {
final ToDoManager toDoManager;

ToDoScreen({required this.toDoManager});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: ListView.builder(
itemCount: toDoManager.toDoItems.length,
itemBuilder: (context, index) {
final item = toDoManager.toDoItems[index];
return ListTile(
title: Text(item.title),
leading: Checkbox(
value: item.isCompleted,
onChanged: (_) => toDoManager.markItemAsCompleted(index),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => toDoManager.deleteItem(index),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
String newTitle = '';
return AlertDialog(
title: Text('New To-Do Item'),
content: TextField(
onChanged: (value) => newTitle = value,
),
actions: [
TextButton(
onPressed: () {
toDoManager.addNewItem(newTitle);
Navigator.pop(context);
},
child: Text('Add'),
),
],
);
},
);
},
),
);
}
}

In this example, the ToDoItem class represents a single to-do item with its properties and the ToDoManager class handles the management of the to-do items by providing methods to add, mark as completed, and delete items. The ToDoScreen class is responsible for rendering the UI and interacting with the ToDoManager to display the list of to-do items and handle user actions like adding and deleting items.

By separating the responsibilities of managing the to-do items and handling the UI, we adhere to the Single Responsibility Principle. The ToDoManager class focuses solely on managing the to-do items, while the ToDoScreen class concentrates on rendering the UI and interacting with the user.

Open-Closed Principle (OCP)

The OCP promotes the idea that software entities (classes, modules, etc.) should be open for extension but closed for modification. In mobile app development, this principle emphasizes designing code that can be easily extended without modifying existing code. By using abstractions, interfaces, and dependency injection, developers can add new features or functionalities without altering the existing implementation. For instance, implementing a plugin architecture for adding new functionalities to a mobile app without modifying the core codebase follows the OCP.

Here’s an example that demonstrates the Open-Closed Principle (OCP) in a Flutter to-do app. We’ll design the code to be open for extension but closed for modification by using abstractions and inheritance.

// to_do_item.dart

abstract class ToDoItem {
String title;
bool isCompleted;

ToDoItem(this.title, {this.isCompleted = false});

void toggleCompletion() {
isCompleted = !isCompleted;
}
}

class BasicToDoItem extends ToDoItem {
BasicToDoItem(String title) : super(title);
}

// to_do_manager.dart

class ToDoManager {
List<ToDoItem> _toDoItems = [];

void addNewItem(ToDoItem item) {
_toDoItems.add(item);
}

void deleteItem(ToDoItem item) {
_toDoItems.remove(item);
}

List<ToDoItem> get toDoItems => _toDoItems;
}

// main.dart

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
final ToDoManager toDoManager = ToDoManager();

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ToDoScreen(toDoManager: toDoManager),
);
}
}

class ToDoScreen extends StatelessWidget {
final ToDoManager toDoManager;

ToDoScreen({required this.toDoManager});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: ListView.builder(
itemCount: toDoManager.toDoItems.length,
itemBuilder: (context, index) {
final item = toDoManager.toDoItems[index];
return ListTile(
title: Text(item.title),
leading: Checkbox(
value: item.isCompleted,
onChanged: (_) => item.toggleCompletion(),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => toDoManager.deleteItem(item),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
String newTitle = '';
return AlertDialog(
title: Text('New To-Do Item'),
content: TextField(
onChanged: (value) => newTitle = value,
),
actions: [
TextButton(
onPressed: () {
final newItem = BasicToDoItem(newTitle);
toDoManager.addNewItem(newItem);
Navigator.pop(context);
},
child: Text('Add'),
),
],
);
},
);
},
),
);
}
}

In this example, we introduce the ToDoItem abstract class that defines the common properties and behavior for a to-do item. We then create a concrete implementation called BasicToDoItem. The ToDoManager class manages a list of ToDoItem instances and provides methods for adding and deleting items.

By using an abstract class and inheritance, we follow the Open-Closed Principle. The ToDoManager class is open for extension, as it can work with any subclass of ToDoItem without modifying its implementation. In the ToDoScreen class, we interact with the ToDoManager through the abstraction provided by ToDoItem. If we want to introduce new types of to-do items, we can simply create new subclasses of ToDoItem without modifying the existing classes.

This adherence to the Open-Closed Principle allows for easy extension of the codebase. Adding new types of to-do items, such as prioritized or recurring items, can be achieved by creating new subclasses of ToDoItem and implementing the required behavior. The ToDoManager class and other parts of the codebase remain unchanged, demonstrating the power of open for extension, closed for modification design principles.

Liskov Substitution Principle (LSP)

The LSP states that objects of a superclass should be replaceable with objects of their subclasses without affecting the correctness of the program. In mobile app development, adhering to the LSP ensures that derived classes can be used interchangeably with their base classes, preserving the expected behavior. This principle allows for easy extensibility and supports the development of modular, reusable code. For example, if a base class represents a generic database handler, any derived class (e.g., SQLite, Firebase) should be able to replace it without causing any issues.

Here’s an example that demonstrates the Liskov Substitution Principle (LSP) in a Flutter to-do app. We’ll create a base class ToDoItem and its derived classes BasicToDoItem and PriorityToDoItem. The derived classes should be substitutable for the base class without affecting the correctness of the program.

// to_do_item.dart

class ToDoItem {
String title;
bool isCompleted;

ToDoItem(this.title, {this.isCompleted = false});

void toggleCompletion() {
isCompleted = !isCompleted;
}
}

class BasicToDoItem extends ToDoItem {
BasicToDoItem(String title) : super(title);
}

class PriorityToDoItem extends ToDoItem {
int priority;

PriorityToDoItem(String title, this.priority) : super(title);
}

// to_do_manager.dart

class ToDoManager {
List<ToDoItem> _toDoItems = [];

void addNewItem(ToDoItem item) {
_toDoItems.add(item);
}

void deleteItem(ToDoItem item) {
_toDoItems.remove(item);
}

List<ToDoItem> get toDoItems => _toDoItems;
}

// main.dart

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
final ToDoManager toDoManager = ToDoManager();

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ToDoScreen(toDoManager: toDoManager),
);
}
}

class ToDoScreen extends StatelessWidget {
final ToDoManager toDoManager;

ToDoScreen({required this.toDoManager});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: ListView.builder(
itemCount: toDoManager.toDoItems.length,
itemBuilder: (context, index) {
final item = toDoManager.toDoItems[index];
return ListTile(
title: Text(item.title),
leading: Checkbox(
value: item.isCompleted,
onChanged: (_) => item.toggleCompletion(),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => toDoManager.deleteItem(item),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
String newTitle = '';
return AlertDialog(
title: Text('New To-Do Item'),
content: TextField(
onChanged: (value) => newTitle = value,
),
actions: [
TextButton(
onPressed: () {
final newItem = BasicToDoItem(newTitle);
toDoManager.addNewItem(newItem);
Navigator.pop(context);
},
child: Text('Add'),
),
],
);
},
);
},
),
);
}
}

By adhering to the Liskov Substitution Principle, the derived classes BasicToDoItem and PriorityToDoItem can be used interchangeably with the base class ToDoItem. The ToDoManager class can add, delete, and work with objects of any of these derived classes without affecting the correctness of the program.

Interface Segregation Principle (ISP)

The ISP suggests that client-specific interfaces are better than one general-purpose interface. In mobile app development, this principle encourages the creation of specific interfaces that cater to the needs of individual clients or components. By having smaller, focused interfaces, apps can avoid unnecessary dependencies and coupling between different modules. This allows for better flexibility, maintainability, and testability. For instance, separating the interfaces for user authentication and data retrieval ensures that each client/component only depends on the relevant interface.

Here’s an example that demonstrates the Interface Segregation Principle (ISP) in a Flutter to-do app. We’ll design interfaces to provide specific sets of functionality and implement them in separate classes.

// to_do_item.dart

class ToDoItem {
String title;
bool isCompleted;

ToDoItem(this.title, {this.isCompleted = false});

void toggleCompletion() {
isCompleted = !isCompleted;
}
}

// to_do_storage.dart

abstract class ToDoStorage {
void saveToDoItem(ToDoItem item);
}

class LocalStorage implements ToDoStorage {
@override
void saveToDoItem(ToDoItem item) {
// Save the item to local storage
}
}

class CloudStorage implements ToDoStorage {
@override
void saveToDoItem(ToDoItem item) {
// Save the item to cloud storage
}
}

// to_do_manager.dart

class ToDoManager {
List<ToDoItem> _toDoItems = [];
ToDoStorage _storage;

ToDoManager(this._storage);

void addNewItem(String title) {
final newItem = ToDoItem(title);
_toDoItems.add(newItem);
_storage.saveToDoItem(newItem);
}

void deleteItem(ToDoItem item) {
_toDoItems.remove(item);
}

List<ToDoItem> get toDoItems => _toDoItems;
}

// main.dart

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
final ToDoManager toDoManager = ToDoManager(LocalStorage());

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ToDoScreen(toDoManager: toDoManager),
);
}
}

class ToDoScreen extends StatelessWidget {
final ToDoManager toDoManager;

ToDoScreen({required this.toDoManager});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: ListView.builder(
itemCount: toDoManager.toDoItems.length,
itemBuilder: (context, index) {
final item = toDoManager.toDoItems[index];
return ListTile(
title: Text(item.title),
leading: Checkbox(
value: item.isCompleted,
onChanged: (_) => item.toggleCompletion(),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => toDoManager.deleteItem(item),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
String newTitle = '';
return AlertDialog(
title: Text('New To-Do Item'),
content: TextField(
onChanged: (value) => newTitle = value,
),
actions: [
TextButton(
onPressed: () {
toDoManager.addNewItem(newTitle);
Navigator.pop(context);
},
child: Text('Add'),
),
],
);
},
);
},
),
);
}
}

Certainly! Here’s an example that demonstrates the Interface Segregation Principle (ISP) in a Flutter to-do app. We’ll design interfaces to provide specific sets of functionality and implement them in separate classes.

// to_do_item.dart

class ToDoItem {
String title;
bool isCompleted;

ToDoItem(this.title, {this.isCompleted = false});

void toggleCompletion() {
isCompleted = !isCompleted;
}
}

// to_do_storage.dart

abstract class ToDoStorage {
void saveToDoItem(ToDoItem item);
}

class LocalStorage implements ToDoStorage {
@override
void saveToDoItem(ToDoItem item) {
// Save the item to local storage
}
}

class CloudStorage implements ToDoStorage {
@override
void saveToDoItem(ToDoItem item) {
// Save the item to cloud storage
}
}

// to_do_manager.dart

class ToDoManager {
List<ToDoItem> _toDoItems = [];
ToDoStorage _storage;

ToDoManager(this._storage);

void addNewItem(String title) {
final newItem = ToDoItem(title);
_toDoItems.add(newItem);
_storage.saveToDoItem(newItem);
}

void deleteItem(ToDoItem item) {
_toDoItems.remove(item);
}

List<ToDoItem> get toDoItems => _toDoItems;
}

// main.dart

import 'package:flutter/material.dart';

void main() {
final ToDoStorage storage = LocalStorage();
final ToDoManager toDoManager = ToDoManager(storage);

runApp(MyApp(toDoManager: toDoManager));
}

class MyApp extends StatelessWidget {
final ToDoManager toDoManager;

MyApp({required this.toDoManager});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ToDoScreen(toDoManager: toDoManager),
);
}
}

class ToDoScreen extends StatelessWidget {
final ToDoManager toDoManager;

ToDoScreen({required this.toDoManager});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: ListView.builder(
itemCount: toDoManager.toDoItems.length,
itemBuilder: (context, index) {
final item = toDoManager.toDoItems[index];
return ListTile(
title: Text(item.title),
leading: Checkbox(
value: item.isCompleted,
onChanged: (_) => item.toggleCompletion(),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => toDoManager.deleteItem(item),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
String newTitle = '';
return AlertDialog(
title: Text('New To-Do Item'),
content: TextField(
onChanged: (value) => newTitle = value,
),
actions: [
TextButton(
onPressed: () {
toDoManager.addNewItem(newTitle);
Navigator.pop(context);
},
child: Text('Add'),
),
],
);
},
);
},
),
);
}
}

In this example, we have separate interfaces and classes for different responsibilities. The ToDoStorage interface defines the contract for saving to-do items, and we have two classes implementing it: LocalStorage and CloudStorage. The ToDoManager class handles the management of to-do items and uses the appropriate storage implementation based on its interface.

By segregating the interfaces, we adhere to the Interface Segregation Principle. The ToDoManager class is only concerned with adding, deleting, and retrieving to-do items, while the ToDoStorage interface provides a specific set of methods related to storage. This allows the ToDoManager class to be independent of the storage implementation and promotes a more modular and maintainable codebase.

Dependency Inversion Principle (DIP)

The DIP emphasizes that high-level modules/classes should not depend on low-level modules/classes directly but instead rely on abstractions. In mobile app development, this principle promotes loose coupling and enables easy replacement of implementations. By relying on interfaces or abstract classes, rather than concrete implementations, changes in low-level modules do not affect high-level modules. This principle facilitates modular development, testability, and extensibility. For example, using dependency injection frameworks in mobile app development allows for easy swapping of implementations without modifying dependent classes.

Certainly! Here’s an example that demonstrates the Dependency Inversion Principle (DIP) in a Flutter to-do app. We’ll use dependency injection to invert the dependencies and decouple the higher-level classes from the lower-level implementations.

// to_do_item.dart

class ToDoItem {
String title;
bool isCompleted;

ToDoItem(this.title, {this.isCompleted = false});

void toggleCompletion() {
isCompleted = !isCompleted;
}
}

// to_do_storage.dart

abstract class ToDoStorage {
void saveToDoItem(ToDoItem item);
}

class LocalStorage implements ToDoStorage {
@override
void saveToDoItem(ToDoItem item) {
// Save the item to local storage
}
}

class CloudStorage implements ToDoStorage {
@override
void saveToDoItem(ToDoItem item) {
// Save the item to cloud storage
}
}

// to_do_manager.dart

class ToDoManager {
List<ToDoItem> _toDoItems = [];
ToDoStorage _storage;

ToDoManager(this._storage);

void addNewItem(String title) {
final newItem = ToDoItem(title);
_toDoItems.add(newItem);
_storage.saveToDoItem(newItem);
}

void deleteItem(ToDoItem item) {
_toDoItems.remove(item);
}

List<ToDoItem> get toDoItems => _toDoItems;
}

// main.dart

import 'package:flutter/material.dart';

void main() {
final ToDoStorage storage = LocalStorage();
final ToDoManager toDoManager = ToDoManager(storage);

runApp(MyApp(toDoManager: toDoManager));
}

class MyApp extends StatelessWidget {
final ToDoManager toDoManager;

MyApp({required this.toDoManager});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ToDoScreen(toDoManager: toDoManager),
);
}
}

class ToDoScreen extends StatelessWidget {
final ToDoManager toDoManager;

ToDoScreen({required this.toDoManager});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: ListView.builder(
itemCount: toDoManager.toDoItems.length,
itemBuilder: (context, index) {
final item = toDoManager.toDoItems[index];
return ListTile(
title: Text(item.title),
leading: Checkbox(
value: item.isCompleted,
onChanged: (_) => item.toggleCompletion(),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => toDoManager.deleteItem(item),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
String newTitle = '';
return AlertDialog(
title: Text('New To-Do Item'),
content: TextField(
onChanged: (value) => newTitle = value,
),
actions: [
TextButton(
onPressed: () {
toDoManager.addNewItem(newTitle);
Navigator.pop(context);
},
child: Text('Add'),
),
],
);
},
);
},
),
);
}
}

In this example, we have decoupled the ToDoManager class from the specific storage implementations using dependency injection. The ToDoManager class now relies on the ToDoStorage abstraction instead of concrete implementations like LocalStorage or CloudStorage.

In the main() function, we instantiate the LocalStorage implementation of ToDoStorage and pass it to the ToDoManager constructor. This way, the ToDoManager class depends on the abstraction, not on the implementation details.

By applying the Dependency Inversion Principle, we promote loose coupling and easier extensibility. It allows us to switch storage implementations or introduce new ones without modifying the higher-level ToDoManager class.

Conclusion

The SOLID principles provide invaluable guidelines for mobile app developers to write clean, maintainable, and extensible code. By adhering to these principles, developers can create apps that are resilient to change, easy to understand, and straightforward to maintain. Implementing the SOLID principles in mobile app development not only improves code quality but also enhances collaboration among team members, simplifies debugging and testing, and enables scalability. By embracing the SOLID principles, developers can elevate their mobile app development skills and build high-quality apps that stand the test of time.

👏🏽 Give this story a CLAP

👉🏽 Subscribe for upcoming articles

💰 Access Free Mobile Development tutorials

🔔 Follow for more

See you on next article 👋

--

--

No responses yet