DEV Community

Apiumhub
Apiumhub

Posted on • Originally published at apiumhub.com on

Tips to start modularizing your old Android app

Context

We work with a legacy project whose development started more than 4 years ago by another company with other standards, practices and experiences.

In this project we work with 2 weeks sprints, always delivering new features and fixing bugs, with very closed scopes where adding technical tasks is difficult. We have been adding common best practices like testing and clean architecture, and we have also added new elements within the Android development environment like Room or Jetpack Compose.

Now we start to face a need: start modularizing the Android app to get all its advantages.

Why modularize your Android app?

There are several benefits to having your app modularized:

  • Faster builds. Gradle speeds up the compilation time of your project by allowing you to do tasks in parallel, and reusing all the code already compiled without modifying it. When you start a new project you may not appreciate the compilation time, but when your project grows it is common to have compilation times around 5 minutes. To adjust small details of the view, you have to compile your project between 3 and 5 times, thus accumulating dead time.
  • It tends to simplify development. Working in small modules focused on a single functionality, makes it easier to correct, maintain and improve the code. Just by the simple fact that you limit the size of the code, reviewing or adding tests to start a refactor becomes more comfortable.
  • Reuse modules between apps. Perhaps one of your modules can easily become a library and you can open it to the open source community with all the benefits without affecting your entire app, both in terms of security and integration.
  • It makes it easier to add more people to the team. By diving your code into smaller modules, you can assign people or teams to them directly without affecting the rest of the app. For example, if you have a chat feature in your Android app, and you need help to reach a deadline with a functionality, just having to understand that module without having to understand the rest of the app makes it much faster for you to receive or give help.
  • Improved test automation. By being able to test modules without having to do the whole flow, tests will go faster and errors from previous steps are not propagated. You can create specific rules in your CI to speed up your compilation time, for example only activate UI tests, usually the slowest ones, when merging to your main development branch.

How can we start modularizing an Android app without stopping development or affect our ability to deliver?

When facing such a big challenge we tend to get overwhelmed, but the most important advice is to have patience. In the end, we will find the first step of the way and gradually we will be adding modules until we have it completely modularized.

In this article, we are not looking for a perfect recipe on how to modularize your app. Every project is different and approaching it with strict steps can be more of a problem than a solution. For example, if we were told that in our project we will start with the networking part, it would be chaotic and we would fail in the attempt to see that we have some dependencies in analytics and error display. Just to create these two modules that will use networking, we could lose a couple of weeks to have everything tested and make sure we have not broken anything. And back to the context, we don’t have these two weeks to focus on technical tasks. We must adapt to our situation and look for ways to add small modules to be able to address networking, and do it in the typical sprint endings with some space for external tasks.

  • Analyze your dependencies. Look for a small functionality that has few dependencies or better yet, none. In our case it was all the functional programming part and all the kotlin extensions we have created over the years. We created the module, moved all the code there, tests included, and compiled. Eureka! It worked! Easy? Well, we had to change all the imports of the project, more than 100 modified files, check that we had not broken any test and that all the functionalities of the app were still intact, but we had our first module.
  • Test before you start. A good practice that we should have in our projects would is to have a good test base, but sometimes we get to a project and it is not like that. Therefore, if we find that small functionality without dependencies, before moving it to the new module, we will create some tests that wrap and migrate everything to the new module, and hopefully change imports. We were able to do the first module because we already had a good base of tests that covered this part.
  • Using Gradle to compose dependencies. One of the drawbacks that we can find when modularizing our project can be the management of dependencies and versions. It is often the typical error of compilation of version conflicts of the Android X library. To do this, we can take advantage of Gradle to compose our build.gradle files. In our project we have a dependencies.gradle file where we first define the versions of our dependencies and then the dependencies themselves.

versions = [
"koin_version" : "3.1.4",
]

libs = [
    "koin" : "io.insert-koin:koin-android:$versions.koin_version",
    "koinCore" : "io.insert-koin:koin-core:$versions.koin_version",
    "koinJavaCompat" : "io.insert-koin:koin-android-compat:$versions.koin_version",
    "koinCompose" : "io.insert-koin:koin-androidx-compose:$versions.koin_version",
]

Enter fullscreen mode Exit fullscreen mode

As you can see, when using koin we have several dependencies that share version, and if we had to update it to solve some error, we only have to come here and not look in all the Gradle files that we have in each module of the project.

But, if we have many modules, we will have many build.gradle files and we can take advantage of the composition to save a lot of repeated code. If we define a file with the basic configuration of all the android modules, the only thing we will have to add in each file is a line of code and the particular dependencies of that one.

Android_lib.gradle
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'kotlin-kapt'
apply from: "$rootDir/buildsystem/dependencies.gradle"

android {
  compileSdkVersion configs.compileSdk

  defaultConfig {
    targetSdkVersion configs.targetSdk
    minSdkVersion configs.minSdk

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    consumerProguardFiles "consumer-rules.pro"

  }

  buildTypes {

    debug {
    debuggable = true
    }

    release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
  kotlinOptions {
    jvmTarget = '1.8'
  }
  sourceSets {
    main.java.srcDirs += 'src/main/kotlin'
    test.java.srcDirs += 'src/test/kotlin'
    androidTest.java.srcDirs += 'src/androidTest/kotlin'
  }
  buildFeatures {
    compose true
  }
  composeOptions {
    kotlinCompilerExtensionVersion '1.0.1'
  }
}

Enter fullscreen mode Exit fullscreen mode

With this definition we can compose our dependencies in the new modules.

module.gradle

plugins {
  id 'com.android.library'
  id 'org.jetbrains.kotlin.android'
  id 'kotlin-kapt'
}

apply from: "$rootDir/buildsystem/android_lib.gradle"

android {
  buildFeatures {
    compose true
  }

  composeOptions {
    kotlinCompilerExtensionVersion '1.0.1'
  }
}

dependencies {

  implementation libs.core
  implementation libs.appcompat
  implementation libs.material

  //COMPOSE
  implementation libs.composeActivities
  implementation libs.composeMaterial
  implementation libs.composeConstraintLayout
  implementation libs.composeTooling
  implementation libs.composeRuntime
  implementation libs.composeViewModel

  //Room
  implementation libs.roomRuntime
  implementation libs.roomKtx
  kapt libs.roomCompiler

  //Koin
  implementation libs.koin
  implementation libs.koinJavaCompat
  implementation libs.koinCompose
  implementation libs.koinCore

  //TEST
  testImplementation testLibs.junit
  testImplementation testLibs.coroutinesTest
}

Enter fullscreen mode Exit fullscreen mode

We can even take advantage of gradle to compose even more dependencies and not have to add or update many files when adding a new one shared by several modules.

  • Use the Readme as a knowledge base. Within our repositories the Readme.md file usually contains steps to compile the project, it is the most common in closed projects. The most elaborated documentation is usually found in external solutions such as Confluence, Notion, etc. An interesting option is to have in each module a Readme file that contains some basic information. This information can be a description of the module’s functionality, its dependencies on other modules (with their relative link to facilitate navigability), relevant external dependencies, possible improvements to be made, etc.This makes it easier for new members to join the team, just by giving them access to the repository they can navigate through the simple documentation without having to navigate through code that is much more confusing and intimidating. One consideration is that Android Studio, at the time of writing this, does not have good MD support. A good alternative is Visual Studio Code where you can preview how your file would look like in the repository.

Top comments (0)