Modularizing your Android app, breaking the monolith (Part 1)


Inspired by a Martin Fowlers post about Micro Frontends, I decided to break my monolithic app into a modular app. I tried to read a little more about breaking monolithic apps in Android, and as far as I got, I felt confident to share my experience with you. This will be some series of blog posts where we actually try to break a simple app into a modularized Android app.

_Note: You should know that I am no expert in this, so if there are false statements or mistakes please feel free to criticize, for the sake of a better development. _

What do you benefit from this approach:

  • Well, people are moving pretty fast nowadays and delivery is required faster and faster. So, in order to achieve this, modularising Android apps is really necessary.
  • You can share features across different apps.
  •  Independent teams and less problems per each.
  • Conditional features update.
  • Quicker debugging and fixing.
  • A feature delay doesn’t delay the whole app.

As per writing tests, there is not too much difference about being in a monolith or having a modularized Android app. So we will skip tests on this series. Just make sure that each test stands in the right module which corresponds to the chosen feature.
 Now, there is a small benefit if you are thinking Android specifically:

  • Significantly reduces APK size. Which means more installs (according to some 🌚)

** A small introduction:**
The app actually is pretty simple. Is built with Architecture Components and has only one Activity. Each fragment has some dependencies but they are Singletons, like database reference, or Picasso and the Retrofit API interface.

So basically, this is my application schema:

All my fragments are pulling dependencies from my Application level. Except from one of them. That fragment is totally unrelated to the other part of the app. It just shows some hardcoded values in the RecyclerView. That really looks like a nice way to start.

Note: I also have an AlarmManager and 2 WorkManagers both pulling dependencies from the application layer, which will be covered later.

But first things first, let’s set up gradle for a multi module project. What I mean is that there is no need to reimplement dependencies over and over again when I add a new Android module for some new feature. Instead, we can just put all of our external libraries and dependencies inside the project level gradle and let all the other modules gradle inherit from it. That would bring better management when library updates occur. A smart thing I found in this YouTube video:

ext {

    compileSDKVersionValue = 29
    minSDKVersionValue = 22
    targetSDKVersionValue = 29

    libraries = [
            cardView              : 'androidx.cardview:cardview:1.0.0',
            androidXLegacySupport : 'androidx.legacy:legacy-support-v4:1.0.0',
            androidXAppCompat     : 'androidx.appcompat:appcompat:1.1.0',
            lifecycleExtension    : 'androidx.lifecycle:lifecycle-extensions:2.1.0',
            viewModelKtx          : 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc01',
            constraintLayout      : 'androidx.constraintlayout:constraintlayout:2.0.0-beta3',
            fragmentNavigation    : 'androidx.navigation:navigation-fragment-ktx:2.2.0-rc01',
            fragmentNavigationKtx : 'androidx.navigation:navigation-ui-ktx:2.2.0-rc01',
            retrofit              : 'com.squareup.retrofit2:retrofit:2.6.1',
            loggingInterceptor    : 'com.squareup.okhttp3:logging-interceptor:4.1.0',
            dagger                : '',
            coroutinesCore        : 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2',
            livedataKtx           : 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc01',
            picasso               : 'com.squareup.picasso:picasso:2.71828',
            materialDialogCore    : 'com.afollestad.material-dialogs:core:2.8.1',
            materialDialogInputs  : 'com.afollestad.material-dialogs:input:3.0.0-rc2',
            room                  : '',
            roomKtx               : '',
            jodaTime              : 'net.danlew:android.joda:2.10.2',
            rocket                : 'com.github.stavro96:Rocket:1.2.0',
            paging                : 'androidx.paging:paging-runtime:2.1.0',
            fragment              : 'androidx.fragment:fragment:1.2.0-rc01',
            fragmentKtx           : 'androidx.fragment:fragment-ktx:1.2.0-rc01',
            workManager           : '',
            viewModelSavedState   : 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-rc01',
            retrofitMoshiConverter: 'com.squareup.retrofit2:converter-moshi:2.4.0',
            moshiCore             : 'com.squareup.moshi:moshi:1.8.0',
            moshiKotlin           : 'com.squareup.moshi:moshi-kotlin:1.6.0',
            smoothie              : 'com.github.stavro96:smoothie:1.0',
            assistedInject        : 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.0',
            kotlin                : 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version',
            coreKtx               : 'androidx.core:core-ktx:1.2.0-beta01',
            airBnBLottie          : '',
            firebase              : '',
            crashLytics           : '',
            locationService       : ''

    annotationProcessors = [
            moshiKapt         : 'com.squareup.moshi:moshi-kotlin-codegen:1.8.0',
            daggerKapt        : '',
            assistedInjectKapt: 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0',
            roomKapt          : ''

    testImplementations = [
            coroutinesTest        : 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2',
            jUnit                 : 'junit:junit:4.12',
            mockitoCore           : 'org.mockito:mockito-core:2.28.2',
            jodaTimeTest          : 'joda-time:joda-time:2.10.2',
            mockitoKotlin         : 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0',
            mockitoInline         : 'org.mockito:mockito-inline:2.13.0',
            androidArchCoreTesting: 'android.arch.core:core-testing:1.1.1',
            coroutinesTesting     : 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2',
            mockWebServer         : 'com.squareup.okhttp3:mockwebserver:4.1.0'

    androidTestImplementations = [
            espressoCore            : 'androidx.test.espresso:espresso-core:3.2.0',
            espressoContrib         : 'androidx.test.espresso:espresso-contrib:3.2.0',
            espressoIntents         : 'androidx.test.espresso:espresso-intents:3.2.0',
            espressoAccessibility   : 'androidx.test.espresso:espresso-accessibility:3.2.0',
            espressoWeb             : 'androidx.test.espresso:espresso-web:3.2.0',
            espressoIdlingConcurrent: 'androidx.test.espresso.idling:idling-concurrent:3.2.0',
            testRunner              : 'androidx.test:runner:1.2.0',
            testRules               : 'androidx.test:rules:1.2.0',
            androidJUnit            : 'androidx.test.ext:junit:1.1.1',
            fragmentTesting         : 'androidx.fragment:fragment-testing:1.2.0-rc01',
            mockitoAndroid          : 'org.mockito:mockito-android:2.24.5',
            workerTest              : ''

    roomIncrementalAnnotationProcessor = [
            "room.schemaLocation"  : "$projectDir/schemas".toString(),
            "room.incremental"     : "true",
            "room.expandProjection": "true"]

This would keep all your build.gradle files super clean. Now you don’t have to require a new dependency and manage the updates because you deal with it only once. And this is how you call them after:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation libraries.fragmentNavigationKtx
    implementation libraries.fragmentNavigation
    implementation libraries.viewModelKtx

Note: You must apply plugin : ‘kotlin-kapt’ when creating a new module.

Basically, this is all what my new module needs. And now let’s break something.

Create a new Android Module and just move all your new features classes over there. Don’t forget layouts, strings, dimens, drawable and all resources that are unrelated to other fragments. Get them too. For the current case, there will be no errors because I inherit nothing from any of my app components.

All I have to do now, is just apply this feature to my app/build.gradle level.

Note: The nav graph should break because moving fragment outside the module would bring to unresolved element, but not to worry, we will fix this right now:

dependencies { of the dependencies 
    implementation project(':feature_6_module') 
    //the naming is not like that in my original project

Done. The application schema now would look like this:

Now, my feature_6_module is installed as an “external library” to my app module.


This was part 1 of breaking a monolithic app into a modularized app in Android. Next part will be all about Dagger and core dependencies.

Stavro Xhardha