Adoption of Clean Architecture layers with modules
Good architecture is a key to build the modular, scalable, maintainable and testable application.
clean architecture mainly allows to separate business logic and scales well.
Brief into to Clean Architecture
Without going into too many details about Clean Architecture we will define 3 layers, each having a distinct set of responsibilities:
Presentation layer :presents data to a screen and handle user interactions
Domain layer :contains business logic
Data layer :access, retrieve and manage application data
The core aspect of CA is proper layer separation (dependency rule) where the domain layer is independent of any other layers:
domain layer we should not access any classes defined in other (outer) layers
By preserving this rule (boundary) we gain confidence that any change applied to data or presentation layers would not affect the domain layer ( this is the core idea behind CA).
- Update on Data layer by migration of DB Engine like SQLight to Realm should not effect Domain and Presentation layer.
- If we modify any UI Component in Presentation layer the Data and Domain Layer should not be effected.
Ideally, the domain layer should be independent of libraries and frameworks. It is fine to have Kotlin Standard Library and some dependency injection library dependency, but we should always try and avoid other libraries and frameworks in the domain layer (especially the Android framework).
Multiple Approach to Adopt Clean Architecture
The simplest approach would be to have 3 packages (presentation, domain, data) inside the app module.
Approach 1- Single App module
This approach works, but it has a few serious flaws. First of all, there is no mechanism of layer dependency verification, meaning that we can access (usually by accident) classes from the presentation or data layers inside the domain layer breaking the core assumption of Clean Architecture.
Approach 2- One Module per layer
To enforce layer separation we could create separate modules for each layer and define dependencies between them.
This solution looks similar to the first one, so let’s look at Android Studio project view to see the difference:
In this configuration presentation module usually substitute app module, however it still servers as the main application module.
By separating layer into different modules we gain the layer separation confidence and a bit of scalability, but the approach still has scalability problems — while implementing or removing feature we would have to jump across multiple modules. Code may be accidentally coupled between features and this adds hidden cross-feature dependencies that may be hard to track and maintain.
Approach 3 — Layers inside the feature module
We can move data ,presentation ,domain layers inside the (feature) module, but this time each feature will contain its own set of 3 layers:
This time app module wires everything together.
Usually package structure of the app module is a bit different than feature modules, because it mostly contains “fundamental app configuration” (dependency injection, application class, retrofit configurations, etc.) and code that wire multiple module together (eg. some internal event bus).
Looking at Android Studio project view we will see something like this:
With this approach, advantages are:
- Proper code separation from a feature perspective and more solid cross-feature boundaries. It is much easier to define cross-feature dependencies and certain features may depend on 3rd party libraries that are not needed for other features.
- On top of that Gradle adds some caching where only some of thee modules are may be recompiled instead of the whole project.
- Feature modules we can take advantage of Android dynamic delivery.
- Proper feature ownership per team (eg. the team X can work on code stored within a single feature module) and unit tests separation(unit tests for a feature are contained within feature module).
- Shared modules eg. feature_base module containing common classes (BaseActivity, BaseFragment etc.) or modules that contain some common resources/code to avoid duplication.
Compare approaches
Below table presents a brief summary of all the approaches:
we could start with approach 1 and overtime slowly migrate project into approach 2, approach 3 however, migration is not always that straight forward (due to hidden dependencies). It’s much more cost (dev time) effective to start a project with properly separated features.