In this article, I will demonstrate how I modularized my existing project, creating structure and clarity for both the project and myself. I’ll showcase this through a concrete example and how I broke down my project into modules.
Basics of Modularization
In a growing codebase, maintainability can suffer due to its increasing size and lack of a well-defined structure. Modularization is the practice of organizing the codebase into independent, self-contained parts or modules, enhancing maintainability by reducing complexity and facilitating easier problem-solving through division into manageable subproblems.
Benefits of modularization
The benefits of modularization focus on improving codebase maintainability and overall quality. Here’s a summarized table highlighting the key benefits:
- Reusability: Modules allow for code sharing and building multiple apps from a common foundation, enhancing reusability.
- Strict visibility control: Easily manage what is exposed to other parts of the codebase, promoting controlled access and usage.
- Customizable delivery: Enables conditional or on-demand delivery of specific app features using Play Feature Delivery and app bundles.
Modularization offers unique advantages that include:
- Scalability: Reduces tight coupling, limiting the ripple effects of changes and promoting contributor autonomy.
- Ownership: Assigns dedicated owners to modules, ensuring accountability for code maintenance and improvements.
- Encapsulation: Encourages isolated code with minimal knowledge about other parts, aiding readability and understanding.
- Testability: Enhances the ease of testing components in isolation, facilitating better code testing.
- Build time improvements: Utilizes Gradle functionalities like incremental build, build cache, or parallel build to enhance build performance through modularity.
Types of modules
Data modules
A data module typically encompasses repositories, data sources, and model classes. Its primary responsibilities include:
- Encapsulation of Data and Business Logic for a Domain: Each data module handles data relevant to a specific domain, encompassing various related data types.
- Exposing the Repository as an External API: The public API of a data module is typically the repository, responsible for providing access to the data for the rest of the application.
- Hiding Implementation Details and Data Sources: Data sources within a data module should only be accessible by repositories within the same module, ensuring they are hidden from external access. Kotlin’s private or internal visibility keywords can enforce this restriction.”
Feature modules
A feature refers to a distinct section of an app’s functionality, often corresponding to a screen or a set of closely related screens, such as a sign-up process or a checkout flow. In apps with a bottom bar navigation, each destination typically represents a feature.
Common modules
“Common modules, also referred to as core modules, encompass code utilized frequently by other modules. They mitigate redundancy and do not pertain to any specific layer in the app’s architecture. Here are examples of common modules:
- UI module: This module encapsulates custom UI elements or branding components, promoting reusability across features. It helps maintain a consistent UI design throughout the app, especially when centralized theming is employed.
- Analytics module: Dedicated to tracking and analytics functionalities, this module serves business requirements and can be shared across various components. It provides a centralized approach for handling analytics-related operations.
- Network module: This module is designed to handle network-related operations, providing an HTTP client that can be shared by multiple modules. It’s particularly valuable when custom network configurations are required for the client.
- Utility module: The utility module contains small, reusable code snippets or helper functions that are utilized across the application. Examples include testing helpers, currency formatting functions, email validators, or custom operators.”
Test modules
Test modules in Android are dedicated modules created exclusively for testing. They include test code, test resources, and dependencies necessary for running tests, distinct from components needed during the application’s runtime. The purpose of test modules is to segregate test-specific code from the main application, enhancing manageability and maintenance of the module code.
My project structure
1. build-logic
I have a separate part of the project that is responsible for the common build logic of the modules. For this, I used the Gradle Convention Plugin. You will be able to read more about this in another article in detail.
Convention plugins provide a method to consolidate and reuse standard build configurations within Gradle. Developers can establish a set of conventions for a project or module and then extend these conventions to other projects or modules. This streamlines build management and enhances productivity.
One notable advantage of convention plugins is their compatibility with Kotlin, a contemporary programming language running on the Java Virtual Machine. Kotlin offers various capabilities that make it ideal for creating Gradle plugins, including robust type safety, a succinct syntax, and enhanced support for functional programming.
2. app
The app modules handle notifications and the drawer navigation. The complete navigation graph of the application is assembled here in a single file using includes. Each module has its own navigation graph, which is consolidated by the app module.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mobile_navigation"
app:startDestination="@id/product_list_nav_graph">
<include app:graph="@navigation/product_list_nav_graph" />
<include app:graph="@navigation/options_nav_graph" />
<include app:graph="@navigation/add_sale_nav_graph" />
<include app:graph="@navigation/collection_details_nav_graph" />
<include app:graph="@navigation/product_new_nav_graph" />
<include app:graph="@navigation/product_details_nav_graph" />
</navigation>
I created this according to the official documentation:
https://developer.android.com/guide/navigation/integrations/multi-module
3. core
The core module contains the codebase utilized by multiple feature modules. Let’s take a look at these individually.
In the database module, you’ll find all the necessary classes for the local database used by Room. This includes the definitions for Dao, Entity, Enum converter objects, and the database that consolidates them.
In the model module, you’ll find all the common data classes used by the application, which are used across various data or feature modules. Additionally, the ‘CallResult’ sealed class, utilized by all data modules, is also placed here.
The network module is responsible for accessing the database. Here, you’ll find the necessary Hilt modules for Firebase Realtime Database. This is where the definition for Retrofit, OkHttp, and the API could also reside.
In the storage module, you’ll find the necessary Hilt modules for Firebase Storage.
In the ui module, you’ll find all common custom views, their related classes, Kotlin extensions, and dialog handling. As you can see, within this module, there’s a product module. Here, I’ve gathered classes and extensions specifically needed for the ‘product list’ and ‘product details’ feature modules. I didn’t want to include these in the UI module, but I still wanted to handle them separately from the feature modules, thus avoiding unnecessary duplications.
In the util module, you’ll find extensions and utility classes that extend across the entire application. These include classes for date handling, shared preferences management, and file handling.
4. data
I won’t elaborate on the data modules individually, but in summary, it can be said that they handle the data structure serving the feature modules in a well-separated manner.
A data module can be associated with multiple feature modules to ensure that each feature module has complete access to all the necessary data.
Every data module includes the necessary modules for Hilt, module-specific data classes, and the repository interface and implementation.
5. feature
Each feature module handles its own scope of responsibility, usually covering the functionality of a single screen or a closely related group of screens.
Of course, it can contain additional classes related to implementing a specific functionality for the feature, necessary for that particular feature’s realization. If the same code is needed for multiple feature modules, it’s worth considering extracting these into a sub-module of the core module.
The module also contains resources such as drawables, strings, menus, layouts, and the custom navigation graph which is included in the app module.
Finally, here’s an example of how the modules communicate with each other.