I've had a gnawing feeling at the back of my head ever since I attempted to create my first Android app years ago. I couldn't quite put my finger on what it was, that is until I read the millionth (I have no proof of the contrary, so I'll stick to the millionth) article related to Android Development. While reading it, I thought - "What if I develop an app with responsibility as my primary focus?". This was contrary to my current approach to thinking in screens when developing.
With this thought in mind and my accumulated knowledge over the years, I set out to develop an architecture that is both simple and scalable. This article will function as a way of reflecting on the project and the architecture implemented as well as serve to share my findings. I will explain how I've structured the application and, most importantly, why I have made the decisions that I have.
First, a bit about the project. I've named it Pure Planting, it is a simple application to keep track of your house plants and when they need to get water. You can find the source code on Github here. The app is divided into a few core modules. These modules will be fleshed out in this article:
- business module
- library module
- component module
- app module
Starting with the module that will be the most easily understood, the business module. Its children nodes serve the purpose of housing classes responsible for representing business elements related to the problem the app is aimed to solve. They will be responsible for solving the majority of the business requirements for the project. These modules are more commonly known as domain modules, but that does not tell me the responsibility towards the app. The name "business" tells me that these modules are tied directly to the business behind the app. I see it as the business module is comprised of domain elements, but it doesn't exclusively have to consist just of domain elements. In short, I've named the root module after what the module represents instead of what it consists of.
Pure Planting has two business modules, plant and notification. Each business child module has the following structure:
- Root model named after the module itself - In Pure Planting this would be a model named Plant and Notification
- Any amount of supporting models
- A contract that defines ways to retrieve and store the root model - This is your Repository classes
The structure is basic, which is great for something that represents the beating heart of an application. These modules will contain the vast majority of unit tests in the project. This is because the models housed by the modules are responsible for meeting most of the app's business requirements. For more complex interactions, UseCase classes can be used to coordinate the interaction between multiple domain models.
A default and fake implementation has to be packaged alongside the Repository contracts (interface) and made publicly available for the dependents. A fake class allows the dependent to easily provide a fake in their tests and a default implementation removes the responsibility of each dependent to implement the contract - both of which simplify the usage of the module.
There is another reason why I've decided to give the name business module instead of domain - these modules are Android modules, not pure Kotlin libraries. There are two reasons for it:
- Extend the models with Android's Parcelalbe interface. This interface optimizes the serialization and deserialization processes of models. It improves performance when passing models in Intents or storing them in Bundles.
- The default implementation packaged with the Repository contract requires the module to depend on other library (more on this next up) modules to fetch data from different data sources. This dependency differs from the standard domain module which usually does not provide default implementations of the exposed contracts.
In summary, the business modules are responsible for the following:
- Define domain models
- Define a contract to access these models - Repositories
- Providing a fake and default implementation of the contract
- Defining UseCase classes, if any, that coordinate interactions between domain models
- Internal Mappers to map between domain and data layer models (located in the library modules)
Next, we have a familiar one from other architectural approaches, the library modules. These modules serve to isolate the required capabilities of your app which is handled by 3rd parties. This allows you to develop the contract that you need the library to fulfil. The implementation details are then the concern of the library module itself. If ever the implementation needs to be changed, it can just be done within the specific library module. These modules can contain tests to ensure the implementation meets the requirements of the exposed contract. As with the business modules, the contracts should include a fake and default implementation - this makes the module easier to use and replace.
Pure Planting has the following library modules with their specific responsibilities:
- design - responsible for housing all the colours, themes, and styles used throughout the application.
- db_tables - responsible for housing models that represent tables in a database. They were originally contained in the business modules but were moved out to remove the responsibility of the app module to set up the database implementation. These will contain Entity models that each represent a table in a database.
- database - responsible for housing the app's database implementation
- navigation - was responsible for housing navigation-related classes intended to be dependent upon component modules to allow navigation. - However, these classes ended up only being used by the app module, so this module now serves to expose any 3rd party navigation libraries that are used by the component modules.
Besides isolating functions implemented by 3rd party libraries, the library modules can also contain code and resources shared among the next type of module - components. For example, I've found it useful to contain all the design-related aspects of the app in a library module.
The next module that we have has a single purpose - isolating an app flow. An app flow is a sequence of screens aimed at serving a single purpose. A registration flow is a good example - The aim of which is to create a new user account. Each app flow has an entry point - this entry point will be the only exposed part of our next module, the component module.
The component module consists of the following:
- UI that represents the encapsulated flow's entry point. This can be a Jetpack Compose function or Fragment with XML. This project uses Jetpack Compose which is what I will be referring to throughout the article
- A class responsible for holding the screen's state, coordinating the behaviour required, and persisting the state across configuration change and process death. This class is called a ViewModel
- Supporting models (internal and public)
- All other UI destinations & and their ViewModels
Dependents of a component module - which will be the app module in most cases - will only be able to navigate to the exposed root of the flow encapsulated by the component module. This allows the component module to be updated in isolation when a specific flow changes. For example, the app module should not be affected when requirements change to simplify the registration flow by reducing the number of screens from 4 to 1 (i.e. a single-screen flow). The purpose of the flow has not changed, only its implementation has changed - and implementation changes should not affect dependents as long as the contract remains the same. That is exactly what the component module - and all the other modules! - provides. By defining a purpose and isolating the code that serves that purpose, we can slap a contract above it and expose it as the API of the module. This approach also encourages writing tests, which only further improves the robustness of the module, making it even easier to change implementation details.
Pure Planting consists of 4 component modules, the first is the home module which is the primary destination of the app. The purpose it serves is to display the user's plants. I've named the module home, because that is its primary purpose - acting as the root destination of the app - the displaying of plants can be viewed as a secondary purpose. We then have a module for displaying a list of notifications, view plant details, and a module for creating and editing a plant.
Finally, we have the app module — the most satisfying phase of development — it is now time to put all the isolated pieces we’ve been building up together so that they form the final product. This module has two primary responsibilities. These are:
- Orchestrate the setup of the app-wide navigation
- Instantiate global dependencies
These can be satisfied with an Activity class and an Application class. Depending on what solutions you choose for navigation, dependency injection, or any other Android-specific technologies, the app module can have more responsibilities. These would then be considered secondary responsibilities and should be treated as such — you may later on find a way to isolate that responsibility into a library module, then extracting it will be easier.
In the event of Pure Planting, there are some additional classes related to navigation and dependency injection that can be found in the app module. For navigation, the library used is called SimpleStack. It handles both navigation and dependency injection by making use of Key classes. The app navigates to these Key classes and the Keys are responsible for rendering the UI and providing the dependencies that the UI requires (i.e. ViewModels).
In conclusion, I’ve showcased the structure and philosophy of an Android project with responsibility as the primary focus. The architecture laid out here is still a work in progress. I’ve already started a more complex project with which I aim to discover further improvements to this architecture.
P.S. A topic not touched on in this article is how this approach integrates with the planning and design phase of a project — a topic for a future article.
Originally published on Medium
Thanks for reading the Tapadoo blog. We've been building iOS and Android Apps since 2009. If your business needs an App, or you want advice on anything mobile, please get in touch