loading...

How I made my legacy app modular - part 1

dbottillo profile image Daniele Bottillo ・5 min read

We all have been there. Whatever is the team getting bigger or your app becoming old, there is a point at which to scale up the development of your Android application you start to consider how to modularize the codebase. 

When you reach that point things start to become messy, you may start to think to ditch Gradle in favour of Bazel, or you may think to apply one of the many suggestions on how to create modular apps. But then what about app bundles? and instant app? maybe even dynamic feature? Dagger anyone? More importantly, what about a strategy that works for your own app and your own team? The really question is, how do you move from a monolithic app to anything that can scale up and have more than one module?

You can't definitely do that over night, even for a small application because things will be broken and overlooked, so I would recommend to take a more step by step approach.

Having to tackle this process for the big app of my daily job, I've decided to first modularize a small open source app of my own. It's an application to browse cards for a collectible card game called Magic The Gathering (https://github.com/dbottillo/MTGCardsInfo ). The app is fairly small: less than 10 activities, lots of logic around the database with 1k daily active users so I thought it could have been a good use case to learn the process.

So where to start from? First we should consider the structure of the modules that we want to have, I think the easiest structure is having three modules called app, legacy and core :

  • app is the top level module that it's generating the APK of your project, the idea is that app knows everything below and ideally should contain only the application class which in the beginning can live in any other module
  • core is a library module that contains everything that needs to be shared across modules, eg. api model, util classes, etc.. . 
  • legacy is your current codebase, it lives below app and above core, the reason for this is that ideally your legacy module will be converted in many siblings modules that have core below and will provide your features to your app module

module structure

There are alternatives to this approach, for example you can think of creating core above legacy and pull out code from it but I don't think it's a good idea: if core is above legacy then if you move one class from legacy to core then its dependencies will still be available (remember that each module can still access everything from a module below) so it will not help you to untangle any dependency really.

In the beginning I recommend to make all your modules android modules so it's easier to migrate (in the future you can think of removing the android dependency from them). I also suggest to create a config-android.gradle to import in every other module to share the android configuration, the setup should look something like this:

app/src/main/AndroidManifest.xml
app/build.gradle
app/src/main/java/… → empty
legacy/src/main/AndroidManifest.xml → existing manifest
legacy/build.gradle → existing gradle file
legacy/src/main/java/… → all your existing codebase
core/src/main/AndroidManifest.xml
core/build.gradle
core/src/main/java/… → empty
build.gradle → generic configurations across modules
settings.gradle → project configuration
config-android.gradle → shared android configuration across modules

Let's have a look into the details of each file.

File: app/src/main/AndroidManifest.xml

<manifest package="com.dbottillo.mtgsearchfree.app" 
   xmlns:android="http://schemas.android.com/apk/res/android" 
   xmlns:tools="http://schemas.android.com/tools">
    <application        
           android:name="com.dbottillo.mtgsearchfree.MTGApp"
           android:icon="@drawable/ic_launcher"                   
           android:label="@string/app_name"         
           android:theme="@style/MTGSearchTheme">
    </application>
</manifest>

Nothing particular here, the manifest is pretty much empty, it just refers the Application class that at the moment lives in the legacy module. One consequence of moving application here is that it requires to also move the icon @drawable/ic_launcher to the app src directory as well.

File: app/src/build.gradle

apply plugin: 'com.android.application'
apply from: '../config-android.gradle'
android {    
   defaultConfig {        
      applicationId 'com.dbottillo.mtgsearchfree'        
   }
   signingConfigs {       
      debugConfig { ... }
      releaseConfig { ... }
   }
   buildTypes{
       debug {          
           applicationIdSuffix ".debug"            
           versionNameSuffix "_dev"            
           signingConfig signingConfigs.debugConfig            
           debuggable true
       }
       release {            
           debuggable false            
           signingConfig signingConfigs.releaseConfig        
       }
   }
}
dependencies {    
    implementation project(':MTGSearch')
    // dependencies
}

apply from lets you import the content of another .gradle file, I'm going to go through the details later, for now you can see that some info regarding android are missing (min sdk, target sdk, etc..). That's because they are defined in this other gradle file, this file then just specify thing that are related to the app module. Note that this is the module that you need to use in order to create an APK or run the app on a device.

File: core/src/main/AndroidManifest.xml

<manifest    
    package="com.dbottillo.mtgsearchfree.core"     
    xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

The manifest of core is empty, which at this stage makes sense because the module is empty and therefore no activity, service, etc.. are defined in this module.

File: core/build.gradle
apply plugin: 'com.android.library'
apply from: '../config-android.gradle'
dependencies{    
    // dependencies
}

As the manifest, even the gradle file is quite minimal: it's defined as an com.android.library and it inherits the android configuration.

File: build.gradle
buildscript {
    repositories {        
         google()        
         mavenCentral()        
         maven { url "https://maven.google.com" } 
         jcenter()    
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'        
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3"
    }
}
allprojects {    
    repositories {        
         google()        
         mavenCentral()        
         jcenter()        
         maven { url "https://maven.google.com" }
     }
     dependencies {
         // your dependencies
     }
}
File: settings.gradle
include ':legacy', ':core', ':app'

In the root build.gradle you can define all the repository and dependencies for your other modules and in settings.gradle all the modules available, this is quite a straightforward gradle configuration.
More interesting is the config-android.gradle:

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {    
    compileSdkVersion 28    
    buildToolsVersion '28.0.3'
    defaultConfig {       
          minSdkVersion 21        
          targetSdkVersion 28        
          versionCode 101        
          versionName "3.6.0"    
    }
    buildTypes {        
          release {}        
          debug {}   
    }
}
androidExtensions {    
     experimental = true
}
kapt {    
     useBuildCache = true
}

As I mention earlier, this gradle file is shared across modules so it's defining all those property across them, it's just an easier way to handle changes: otherwise every time you want to bump the version code, for example, you need to do it in every module.

For the legacy module, build.gradle and AndroidManifest.xml are pretty much the same: the legacy/build.gradle file needs to import config-android.gradle and change the the plugin from application to library. It is also required to remove the definition of the application class from the AndroidManifest.xml since it's not defined in the app module.

I think this is the minimal amount required to move from one module to three modules! The next steps are to move the application class to the app module and then to disentangle dependencies when moving classes from legacy to core. Will write about those steps in the next parts :)

If you want to check real code, you can look at the commit of the application that I've mentioned in the beginning: https://github.com/dbottillo/MTGCardsInfo/commit/6ec4a1bd72d79fe13f0b7c602a56aeca32a8c43e
Bear in mind that legacy is actually called MTGSearch and because the project is an actually real project, the complexity of each file is slightly bigger than the one described here.

Discussion

pic
Editor guide
Collapse
igorganapolsky profile image
Igor Ganapolsky

So do you recommend switching to Bazel from Gradle in 2019?

Collapse
dbottillo profile image
Daniele Bottillo Author

I think it really depends on the company and the team. It's a lot of effort and has its own pro and cons, I wouldn't recommend it if you don't have experience and the scale to support it :)

Collapse
santi_guaya profile image
Jose Santiago

What a good article, congratulations.

Have you finished all the steps?

The truth is that you helped me a lot with that step.

Collapse
dbottillo profile image
Daniele Bottillo Author

Hi Jose, thanks for your comment! I'm still writing up to the next part; hopefully it will be ready in the next couple of weeks :)

Collapse
dbottillo profile image