The greatest Android modularization mistake and how to undo it
By: Eric N
Tags: Android App Development, Modularization, Android, AndroidDev, Clean Architecture

One upon a time, there was a singularity named :app module. Then came the smaller monoliths, namely :persistence, :networking and :models

Photo by Clay Banks on Unsplash

Why layers monoliths is a mistake?

They are still monoliths, albeit smaller than :app

All the problem that applies to :app still applies to :networking , :persistence and :models while you get none of the benefits modularization brings

Benefits of modularization

(Incremental) build time

Small and focused module — more productivity while working on a particular feature/ product

Features/ products are more self-sufficient and less inter-dependent so more flexibility in design, implementation and testing

Dynamic delivery of modules

More details in Jeroen’s article 👇

Why is it the greatest mistake?

The Internet is filled with tutorials that tell people to break :app module by layers

Looks familiar?

1 small tech unicorn I know fell for this trap just last year

1 big unicorn I worked for feel for it too and are moving to feature oriented module structure

Current team is also moving in the same direction

In my opinion, the only in benefit modularization by layer is an illusion of progress

What’s the better way?

Feature-first modularization!

How to undo the greatest modularization mistake?

i.e. how to go from layer monoliths to feature modules

Create your feature module and move classes to it bottom up (from data model classes to UseCases/ Interactors then ViewModels/ Presenter) one at a time

Whenever a class blocks your move or causes circular dependency, move it to :app module temporarily. Also, creating interfaces help, temporarily.

A real example

There is a Q&A feature in my current app.

This is the current code structure, with layer monoliths

Q&A feature in layer monoliths

Step 1

Move network data model classes and Retrofit interfaces to newly created Q&A feature module

No big challenge here, and since the domain data model classes (Clean Architecture) do not depend on anything, I move them too:

Step 2

Move the DAOs and Room entities to our newly created feature module

@Dao

abstract class QuestionRoomDao : BaseDao<QuestionEntity>, QuestionDao { //.. }

Since Androidx Room and BaseDao are required, remember to add the following dependencies to your qna/build.gradle

dependencies {

//... implementation "androidx.room:room-runtime:$room_version" // Room

kapt "androidx.room:room-compiler:$room_version" // Room compiler

implementation(project(":persistence") // internal library containing BaseDao }

Bonus: Now that QuestionDao and QuestionRoomDao are together in 1 place, I’d personally recommend to merge them. It does more harm than good to have interface when there is only 1 implementation.

In order to use AndroidX Room though, we need to have a configuration class like MyDb that extends RoomDatabase

MyDb is currently in :persistence module and requires QuestionRoomDao so this means we’ll have a circular dependencies here!

The solution is to move MyDb up the dependency graph to good old :app module 😄

It makes perfect sense since MyDb is supposed to be aware of all the *Entity (Room) and *RoomDao classes in the entire application. At the same time, :app module depends on all feature modules.

Room master configuration file should live in :app module

Step 3

Move the repositories to Q&A feature module

Final result

Notes: the :domain module still doesn’t feel right (doesn’t fit into Uncle Bob’s Clean Architecture) but that’s a separate battle for another day

Bonus step — Move the Use Cases

@Singleton

class GetQuestionsUseCase @Inject constructor(

private val userRepository: UserRepository,

private val repo: QuestionRepository

Since the UserRepository is unfortunately inside the :domain module and it might be a big separate ordeal we don’t want to rush into immediately, we must then:

Extract an interface so it becomes UserRepository (the interface) and UserRepositoryImpl

Create a :user feature module and put the UserRespository interface there (of course :app will depend on :user )

feature module and put the UserRespository interface there (of course will depend on ) Now make :qna depend on :user

depend on Now we can move GetQuestionsUseCase to :qna module

Need more inspiration to organize your code by feature?

Working demo with source code

Conclusion

The road to undo the greatest mistake in modularization is long. The good news is, you don’t have to do it in a big bang fashion. Just repeat the same steps mentioned here for each and every feature one at a time, at your own pace.

One might argue that something important like User or Profile needs to be separated into several module and one might be very right. However, my recommendation is don’t overthink the first step or nothing will ever happen. Just move all user/ profile stuffs into a :user / :profile feature module first. It will then be an order of magnitude easier to analyze the feature and break it down than looking at user/ profile as a part of the :app or the layer monoliths.

Next steps

Remove the out-of-place modules like :domain and :models to better conform with Uncle Bob’s Clean Architecture and to have everything organized by features, not layers.

Feature modules depend on *-common modules and optionally each other

2. Certain features are usually big like profile/ authentication and payment. It would make sense to break it down by sub-features and afterwards layers. For example, “payment” super feature can have the following child modules on

payment-common

payment-a which depends payment-common

payment-b-presentation, payment-b-core (domain and data layers combined)

payment-c-presentation, payment-c-domain, payment-c-data

3. Soon enough you’d end-up with dozens, if not hundred of feature modules/ directory under your project root, it’d then be natural to organize them under a