Clean Architecture: Flutter App
By employing clean architecture, you can design applications with very low coupling independent of technical implementation details. That way, the application becomes easy to maintain and flexible to change. Clean architecture allows us to create architectural boundaries between dependencies which allows components to be intrinsically testable
In this project, we’ll use the process of creating a CRM application to discuss a Robert C Martin philosophy called Clean Architecture.
Customer Relationship Management (CRM) is a process companies use to manage customer interactions.
We’ll be using Flutter/Dart to create the app.
Use Cases of the Application
A use case, the essence of the application, describes how users will perform tasks or interact with your application.
We’ll implement the following use cases:
Customers
- Get all customers
- Create customer
- Update customer information
- Make customer inactive
- Make customer active
- Delete customer
Leads (Potential Customers )
- Get all leads
- Create lead
- Update lead information
- Convert the lead to customer
Tasks
- Get all activities for customer
- Create customer task
What is Clean Architecture?
“Clean Architecture” was coined by Robert C Martin and is a software design philosophy that organizes code in such a way that business logic is kept separate from technical implementation (databases, APIs, frameworks). This makes application functionality easy to maintain, change and test.
A more contextual front-end application illustration of clean architecture would be the following image which illustrates the flow of control and data.
Before we start let’s add a few project dependencies. The pubspec.yaml file (located at the root of the project) specifies dependencies that the project requires, such as particular packages (and their versions). Let’s install a few Flutter/Dart dependencies:
- Equatable for object comparison
- Dio for HTTP calls
- Mockito for mocking dependencies in our tests
- dartz to help with functional programming in Dart
- uuid to generate unique ids
How do we organize our code?
I found it useful to divide the project into the following files and folders
├── core
│ └── error
│ └── failures.dart
├── data
│ ├── data_sources
│ │ ├── implementations
│ │ │ └── api
│ │ │ └── task_datasource_impl.dart
│ │ └── interfaces
│ │ ├── customer_datasource.dart
│ │ └── task_datasource.dart
│ └── entities
│ ├── customer_entity.dart
│ └── task_entity.dart
├── domain
│ ├── model
│ │ ├── customer.dart
│ │ └── task.dart
│ ├── repositories
│ │ ├── implementations
│ │ │ ├── customer_repository_impl.dart
│ │ │ └── task_repository_impl.dart
│ │ └── interfaces
│ │ ├── customer_repository.dart
│ │ └── task_repository.dart
│ └── use_cases
│ ├── customer
│ │ ├── create_customer.dart
│ │ ├── delete_customer.dart
│ │ ├── get_all_customers.dart
│ │ ├── get_customer.dart
│ │ ├── make_customer_active.dart
│ │ ├── make_customer_inactive.dart
│ │ └── update_customer_details.dart
│ ├── lead
│ │ ├── convert_lead_to_customer.dart
│ │ ├── create_lead.dart
│ │ ├── delete_lead.dart
│ │ ├── get_all_leads.dart
│ │ ├── get_lead.dart
│ │ └── update_lead_details.dart
│ └── task
│ ├── create_task.dart
│ ├── delete_task.dart
│ ├── mark_task_as_completed.dart
│ └── update_task.dart
├── main.dart
└── presentation
├── components
│ ├── delete_button.dart
│ ├── edit_button.dart
│ ├── list_item.dart
│ ├── list.dart
│ └── toolbar.dart
├── view_models
│ ├── customer
│ │ ├── detail.dart
│ │ ├── edit.dart
│ │ ├── list.dart
│ │ └── new.dart
│ └── task
│ ├── detail.dart
│ ├── edit.dart
│ ├── list.dart
│ └── new.dart
└── views
├── customer
│ ├── detail.dart
│ ├── edit.dart
│ ├── list.dart
│ └── new.dart
└── task
├── detail.dart
├── edit.dart
├── list.dart
└── new.dart
Models
Domain models represent real-world objects that are related to the problem or domain space. This is a good place to start.
Error and Exception Handling
Typically exceptions and errors are caught and handled by using “try-catch” blocks wrapping a piece of code that might throw. We allow errors to bubble up to a point where they can be centrally handled (near the UI).
Languages like Java, allow you used to use the keyword “throws” to mark a function that might have exception side effects. The Dart language does not allow you to mark functions as potentially throwing so you have to remember which functions might throw to handle them accordingly.
There is nothing wrong with this. We would like to however take a different approach, instead of throwing exceptions, we’d like to catch side-effect exceptions and channel the failure to the function’s return value.
This is a Functional Programming (FP) approach to creating pure functions (functions without side effects). The dartz package gives us the ability to write Dart in a more FP way. The package has a type called Either which is used to represent a value that can have two possible types. We’ll use this type as our deterministic return type to either return a Failure or the intended return value.
Let’s define the Failure type and an example of one of many custom-defined failures.
To see this in action, let’s write the customer repository interface/contract.
Use Case: Get All Customers
Before we create the “Get all customers” use case implementation, let’s define an interface/contract for it.
For a continued discussion of the TDD process of writing the implementation code, check out the original post at https://nanosoft.co.za/blog/post/clean-architecture-flutter and the GitHub repo
We’ll store the implementation in the same file:
Conclusion
Using the clean architecture approach/philosophy we can easily develop small decoupled parts of our application in a clean and testable way.