A couple of weeks ago, I tweeted a survey to find out more about how Android developers were modularising their apps. It’s a broad and complicated topic, with no clear standard approach or conventions to follow, so I was interested to learn more.
We had 123 responses — thank you so much to everyone who took the time to fill it out! It was a lot more than I had expected and made for some really interesting reading.
We’ll go over the results from the main questions asked in the survey:
- Number of modules
- Build system used
- How the app is modularised (e.g. by layer or product feature), what a module looks like internally, and any common modules
- How the app handles Navigation
- Any plans to change the modularisation setup in the future
The range of the numbers of modules are was pretty broad — the highest number of modules was 800! However, this was an outlier — the majority of the rest were much lower in comparison:
As you can see, there were a few responses from developers with around the 100–150 module range, some between 60–100, but the majority of responders have less than 30 modules. There were 5 submissions about apps which have only 1 module.
Almost every app is using Gradle as it’s build system — except for two.
The two apps with the highest number of modules (150 and 800) are the two using Buck.
How developers are splitting up modules, and what type of modules varied a lot — some had mostly Android libraries for everything, some had mostly pure Java/Kotlin modules and kept the Android specific code isolated, others had a fairly even mix of both of these. There were also people with modules for WearOS, the NDK, instant apps and using dynamic feature modules.
There were a few themes with how developers approached modularising their apps — some opting to split by their product features, some by core components (e.g.persistence or networking), some by clean architecture. Others used a combination of these — splitting by feature and layer, e.g.
<feature>-data alongside some core modules.
A few people have been following how Plaid modularised and said they are doing the same — each dynamic feature is an
Activity and the app pulls together different Android library feature modules.
Some specific examples:
An app with 120 modules (95% Android modules, 5% pure Kotlin), 11 developers:
We have three types of module:
1. “data” modules. These typically contain a repository for accessing data remotely or via disk. They’re mostly android modules for now, as we depend on Realm and SharedPreference s quite a lot, but we’re working towards converting all of them into pure kotlin modules.
2. “feature” modules. These contain the screens, view models, presenters, etc for a feature. As well as specific use cases. These are always Android modules.
3. “horizontal” modules. These are isolated pieces of functionality that feature modules or data modules can depend on, but not vice versa!
Feature modules only contain the screens for that module, so they’re not shared anywhere. For networking, or APIs that can be shared, they’re kept in data modules (which are meant for re-use).
We have one monster module called
basewhich we created when we first started splitting stuff up. We’re chipping away it constantly, but pulling stuff out of there is really time consuming because it’s so entangled throughout the layers and other modules in the app.
An app with 60 modules (4 Android app, 32 Android library, 24 Kotlin/Java modules), with 6 developers:
A feature is sometimes composed of multiple modules. Firstly the core business rules of the feature are implemented as a Kotlin/Java library that declares interfaces (ports) that must be plugged-into. An Android view layer implementation for the feature plugs into the business rules. An adapter plugs into the business rules to provide an implementation of statistics/analytics gathering (and links it to an analytics client). An adapter plugs into the business rules to provide the integration against a back-end service client, and so on.
We try to develop features as bounded contexts, such that they are not coupled by shared models and entities as much as possible. Data is shared by having an output interface on one feature and an input interface on another. These interfaces form a value boundary. An adapter that handles routing/navigation and understands the interfaces of both features translates between them. For example, selecting from a list on one page, might only cause an ID to be passed to a details page.
This is currently leading to what we call “module explosion” but we predicted this and went into it consciously as we explore this approach and solidify the architectural boundaries that we believe are important. At some point we will probably consolidate a little.
Common modules have been a hot topic of discussion and the ideal approach is currently not decided. We wish to have bounded contexts. We don’t wish to become coupled by too much shared code. However, there are a number of concepts that we don’t want to repeat over and over.
We don’t want a ball of mud ‘utils’ module; we don’t want lots of little single-class, single-function modules. Currently we have a number of small modules. We also consciously decided to have a UI component library with lots of common UI “atoms” and “components” that can be reused across multiple features.
An app with 40 modules (50:50 Android to JVM modules, with 3 developers:
We split our modules by feature and clean architecture layers which include data, business logic (sometimes entities), presentation, and feature (for entry points and DI wiring).
A feature module typically maps to a full screen view along with any background workers for the data primarily involved by the screen.
We have common modules for diagnostics (logging, error reports), Analytics, System (System info, clock, etc;).
An app with 23 modules (70% Android to 30% pure Kotlin modules), with 3 developers:
We support multiple device types can be connected in multiple ways (usb, BT) — these device types have their own ‘interface’ module, each connection lives as a separate implementation module. Then there’s pure business logic, which lives in composeable feature modules. Finally there are composeable UI modules that provide different screens/flows for specific parts of the app, and an app module that ties everything together.
Our business feature modules try to have as few dependencies as possible to focus on the pure business logic. The modules provide interfaces to be able to receive what they need, other modules (such as the app module) provide implementations of these interfaces, like an implementation that uses networking.
Our common modules are a
domainmodule that introduces cross-cutting data classes for the domain, and a
utilsmodule for ‘dumb’ utility methods.
An app with 7 modules (5 Android libraries, 1 pure kotlin, 1 Android application), 5 developers:
Each module represents one UI feature. The main Android module is dependent on all of them. Back-end stuff is in the pure Kotlin module which everything knows about. We aren’t complex enough yet to have much problem with this, when we’ve hit circular dependencies we’ve been able to move things to the correct module without needing a generic
sharedmodule. Although I expect we will need one soon.
Our networking stuff lives in a pure Kotlin module that everything depends on, but each feature module has a repository that adapts from the API classes into the domain objects for that specific feature. We are using an MVVM architecture with Android architecture components, so each feature module has:
- repository (sometimes multiple)
- a ViewModel (with a factory)
- an activity
- a set of binding adapters, invoked from the XML.
We have tried to avoid fragments and custom classes extending from
Viewas much as possible, we think they add too much complexity. Right now we aren’t complex enough to have much shared functionality between the modules. We do have a
UserRepositorywhich is used in its own
loginmodule and other modules, but that’s all right now, so we just gave those other modules a dependency on
login. I can see this becoming more problematic in future though.
The big theme from all of the responses was that navigation is really hard! A lot of the given solutions were cited as areas that could be improved in the future.
The majority have a custom solution — some using a single
Navigator interface in their main
app module and injecting it everywhere its needed. Others enforce that each feature module declares a routing interface which then are individually provided as needed. A few people detailed how they just broadcast what happened (e.g. this button was clicked) and let the top level module with access to everything decide how to proceed, another explained they’re using the Coordinator pattern from the
app module which connects all features together.
One person wrote:
Navigation is currently an area of pain. We are currently using a bespoke navigation component that is too coupled to the navigation UI. We have plans to rewrite navigation and routing.
A module will declare a router interface that the factory assembling it must provide an implementation of. This implementation is then integrated against the global navigation component and raises a “navigation event” which is handled centrally, the navigation UI updated, then a “screen factory” causes the new screen to be displayed.
Plans for the future
There were a few themes here: some wanted to try out a build system other than Gradle to see if they could improve their build times. Lots wanted to improve their navigation or their dependency injection setup (most of these were around dagger-android and moving towards a separate component per module). Others said they wanted to leverage instant apps and dynamic features. Some were happy — they said their setup works well enough for now, and were instead focusing on building app features.
This was fairly tricky to summarise, because of the huge variation in how everyone has chosen to go about modularising. I’ve tried to highlight the trends from results where there have been some, but this is a very broad and complicated topic — please leave a comment if you have anything to add!