While Android Studio, Gradle and Kotlin are getting better and better with every release, the real performance bottleneck in modern android development is kapt — kotlin's compiler plugin responsible for annotations processing (you don't write in Java anymore do you?). The chances are pretty high that you use Dagger or other annotation processors to generate code.
Also See: What Is Android System WebView App?
Developers switch git branches dozens of times per day. The code is different in different branches, and the problem is that kapt does not really know what the consequences of these differences are. So, unfortunately, even though a developer works on at most 10% of the whole app functionality, it's quite common to do a clean/rebuild of 100% of the project after every branch change.
As the app gets bigger, it’s becoming more and more important to break it into separate modules so that we can enjoy the delights of parallel compilation.
How exactly we are supposed to break the app into modules depends, of course, on navigation flows. In the perfect-case scenario the app flow looks something like this:
Everything is usually more complicated in practice:
But nevertheless we always have functional parts naturally separated from the others. Different features/flows normally start from different tabs. If you have a login screen, it’s always a good idea to extract it as it’s needed by everyone.
We can go further and create multiple application modules in one project, so that:
For a simple app with a login screen and two relatively independent features like the one mentioned above, the structure of modules would be the following:
The Main App module should be used only by continuous integration software in order to build, test and deploy the final apk and, very rarely, when we need to test cross-feature flows.
The Feature 1 App and Feature 2 App rectangles are green because they're created only for developers' convenience and whatever we do there has no influence on the production main app.
As you might have noticed, in the realistic example mentioned above the user still navigates from Screen A to F and from E to C. However, the feature modules (feature1 and feature2) can't depend on each other, so their dependencies should be encapsulated.
There can be other horizontal dependencies between these two modules, like common data models or utility classes/functions. Consider putting them in lower-level base modules. Don't follow the temptation to put everything there just in case though: you don't want to have another huge coupled module. Navigation is the only thing that really needs to be encapsulated.
Let's get into the details module by module.
Login module login does not depend on anything (only low level base modules probably), and defines an interface LoginNavigation. I know everything about login sequence, but I don’t know where to go after — this is what this module is about.
Feature module feature1 also does not depend on anything, contains all logic required for screens A, B and C, and defines an interface Feature1Navigation which is responsible for all external navigation. This interface has some human readable methods like goToF(), but no specific activity/fragment name is known at the moment here.
This is the module where a developer or a team responsible for feature 1 is going to work on everyday, enjoying its short compilation time because they don’t need to compile ten other features.
Feature 1 App module, feature1app, depends on login and feature1. It defines its own implementation of LoginNavigation that leads user directly to screen A. It also defines its simple "go there don’t know where" StubFeature1Navigation, but we do not care too much because it should not be used too often (or reconsider module boundaries otherwise).
Feature 1 App is going to be compiled and launched every day. However, the code will almost never change here, because it's just an executable container for feature1.
Main App Module, app, depends on all other modules feature1, feature2, login, and is the only module that should know what the real dependencies are. Apart from that, it should not do much though. It’s just a container that defines the main screen of the app and the dependencies between all navigation flows. I don’t care what exactly login flow consists of, but in the end it should navigate to MainScreen. I don’t care how many screens Feature 1 has or what internal Feature 1 flow is, but I know that when it wants to goToF(), it should navigate to Screen F from Feature 2.
The whole structure of modules with injected dependencies will look like this: