Comprehensive Guide about Multi-module projects in KMP World
Hello friends 👋! I trust you’re doing well. Today, I’ll be sharing some insights I gained this year while working on multi-module projects in the Kotlin Multiplatform (KMP) ecosystem. I’ll illustrate these with examples from my MovieDB-App repository. I hope you find the content enjoyable and informative 🤗.
Strategies
The most common strategies to modularize a project is modularization by layer, or modularization by feature. A great article about these two strategies you can see here 🔗. You might be wondering “What is the better strategy?”, I do not have this answer 😅, I think it depends on your context. You should consider the following aspects:
How many screens does your app have, and how is the navigation structured?
What are the data sources in your project? Do you fetch data from multiple places, and is there a Repository pattern implemented?
How experienced is your team with modularization? Are they familiar with the chosen architecture?
If more than one squad is working on the same code base, have you encountered merging conflicts? Where do these conflicts typically arise?
Is there a design system implemented to maintain consistency in the user interface and experience?
If you have any other questions that are important to consider, please, put in the comments 🙏
Single Responsibility Principle ⚠️
No matter which strategy you are using, you need to define a responsibility for each module. For example, in the MovieDB-App, I have the following scenario:
- composeApp module ➡️ defines an index of all features;
- each feature module ➡️ responsible for a specific feature;
- domain module ➡️ responsible for defining business rules and the repository interface;
- data ➡️ provides data from various sources via concrete repositories;
- platform ➡️ provides common presentation code.
It’s important for your team to have a clear understanding of module responsibilities 💡. This clarity becomes crucial when questions such as ‘Should we create a new module for this?’ or ‘Where should we place this code?’ inevitably arise.
Common Entry points 🚪
You need to define the entry points of your modules, basically the “doors” 🚪. A great strategy for that is to use the plugin pattern, basically the idea is to define modules like plugins 🔌. For example, I have a module called “login-plugin”, where all implementation related to the login will be there, and I will also have another module called “login-plugin-api” which will be the entry point of the “login-plugin” module. Other modules will use the “login-plugin-api”, instead of “login-plugin”. More details about this approach you can find here 🔗.
In the MovieDB-App, I decided to use a more simpler strategy, so we have the following entry points:
- composeApp module ➡️ No entry points 🔐
- each feature module ➡️ composable function that starts the feature flow🚪;
- domain module ➡️ Use cases 🚪;
- data ➡️ No entry points 🔐;
- platform ➡️ presentation components🚪.
Dependencies ⛓
Now you already know the entry points, we need to map the relationship between our modules. In a modularization by layer, you need to keep in mind the basic rule:
You can replace the sentence “depends on” by “just talks to”. This is the main idea 👋
Regarding the MovieDB-App, we have another approach, the modularization by layer, the diagram is different because there are feature modules:
In MovieDB-App example, we can highlight:
- Feature modules are independent of each other;
- Feature modules depend on domain, and platform;
- Modules that don’t have inter-module dependencies: domain, and platform;
Build scripts 👷
Many modules means lot of boilerplate code in your Gradle files 🐘. How to reduce the boilerplate? It looks like they are all doing a similar thing 😢
A good way to reduce the boilerplate is using build-logic
strategy, where you can create conventional plugins.
Conventional plugins are a powerful tool for reducing boilerplate in Gradle build files. You can create plugins to handle Kotlin Multiplatform configuration, Compose Multiplatform setup, and basic coverage report settings. These individual plugins can be combined into a single conventional plugin called my-module-plugin
. Applying this plugin ensures that your module's basic setup, including KMP, Compose, coverage reports, and detekt, is complete.
If you want to learn more, check out the build-logic
folder in the MovieDB-App repository right here.
If you want to explore this further, a helpful article is available here 🔗. You can also check out another great repository that utilizes this approach here 🔗.
Final tips 💡
According to my experience in this year working with multi-module projects I have some tips:
- Create a diagram showing the relationship between the modules;
- Write a documentation about when and how to create a new module;
- Dependency injection library is fundamental (Koin is awesome 🩶)️;
- Kover to check the test coverage report unified (easy compared to JaCoCo);
- Implement the Repository pattern 🤤;
- … If you have any other tip, please comment here 🙏
I hope you liked the content, the idea of this article is to be a kind of short guide for who is starting to learn about multi-module projects in Kotlin Multiplatform Projects.