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 theAPK
of your project, the idea is that app knows everything below and ideally should contain only theapplication
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 belowapp
and abovecore
, the reason for this is that ideally yourlegacy
module will be converted in many siblings modules that havecore
below and will provide your features to yourapp
module
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.
Top comments (5)
So do you recommend switching to Bazel from Gradle in 2019?
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 :)
What a good article, congratulations.
Have you finished all the steps?
The truth is that you helped me a lot with that step.
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 :)
Part 2 is out: dev.to/dbottillo/how-i-made-my-leg... :)