Android App Modularization Tips

Faruk Toptaş
Android Bits
Published in
5 min readJan 30, 2021
Photo by Markus Spiske on Unsplash

Making Android apps/libraries modular has a lot of benefits. If you are developing a new app you should take care of it.

The list of benefits is longer but I will say the most important ones from my side.

Isolation

Isolating modules will decrease side effects and unpredictable breaks of your code. When code base gets bigger side effects will be much more dangerous. Small and isolated parts/modules will have less risks.

Reusability

Modules can be used across apps. If you want to use mono repo approach for multiple apps every single part of your code will be a module. Also you can push your modules to a maven repository and make it easier to use from another apps. Image loading functionality is a widely used feature

Dependencies

Modules have their internal dependencies which are not exposed to other modules. It makes you to design the behavior independently from the implementation. For example loading image from a url is a widely used feature.

Here is the simplest extension of image loading with a Picasso library. Placing this extension in a separate module will only expose the load function. So we don’t need to add Picasso dependency to our feature modules.

fun ImageView.load(url: String) {
Picasso.get().load(url).into(this)
}

imageloader/build.gradle

implementation 'com.squareup.picasso:picasso:2.71828'

myfeature/build.gradle

implementation project(":imageloader")

It is very easy to change Picasso with Glide or any other image loading library. Because Picasso is internal dependency and can be changed any time without changing our exposed behavior.

Modularization allows us to separate internal and external dependencies.

Focus on Behavior

Separating into modules forces us to build behaviors. Like image loading example we expose only the load() function not anything related to Picasso.

Build speed

If there is no change in a module it will not be built again and again. Only the change modules and the modules depends on changed ones will be build.

And much more..

Here is a great blog post series by Jeroen Mols about multi module approach.

I want to share my multi module experiences and a few tips.

Modularization approaches

There are a few modularization approaches. It depends on your project, team and organization.

Modularize by feature

  • Fits for large projects/teams
  • Highly modularized
  • Features can be used across multiple apps easily
  • Communication between feature modules may be struggling
  • More strict rules
https://jeroenmols.com/blog/2019/03/18/modularizationarchitecture/

With this approach you will have too many modules. The app module in the second approach is divided into feature modules. And some shared codes between features are also moved to a separate module.

Modularize by layer

All feature modules are combined in the app module. This approach is less strict. But you still can use separated feature modules.

  • Less modules, less isolation but management is easier
  • Only layers can be used across multiple apps
  • All features are in app the module. Actually still a monolithic app :)
  • common module contains DTO’s, extensions that can be used by all other modules.
  • network module contains Retrofit related classes, Api interface and interceptors.
  • data module contains data related things like repositories, local/remote repositories.
  • library modules are independent from features or other modules like image loading or analytics library.
  • app module contains all features and ui bound classes like Activity, Fragment, ViewModels etc.

Use a common-module.gradle

Apply this gradle script to all feature modules. This will save you from duplicating gradle scripts for each module.

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion compileSdk
buildToolsVersion buildTools

defaultConfig {
minSdkVersion minSdk
targetSdkVersion targetSdk
versionCode versionCode
versionName versionName
}

kotlinOptions {
jvmTarget = "1.8"
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

buildTypes {
debug {
...

}
release {
...

}
}
}

dependencies {

implementation deps.kotlin
implementation deps.koin
// ... other implementations

// Testing
testImplementation project(":testing")
testImplementation deps.junit
// ...

}

tasks.withType(Test) {
testLogging {
events "started", "passed", "skipped", "failed"
}
}

Resources

Feature specific resources can be in feature modules but separating resources will make it difficult to organize. So keeping them in the common module will be better.

If a multi-lang tool is used to organize strings between Android/iOS clients, this tools usually gives a single strings file per language. You may want to keep strings together.

If dynamic-modules are used you have to take care of resources that may affect app/module size like drawables.

DTO’s and common used extensions can be in common module. Because each feature module can use these.

Jacoco Coverage Report

Each module has its own reports output. So you will need to see all coverage reports in one place. The script below unites all modules coverage reports in one report:

Add this line to each modules build.gradle file

apply from: "$rootDir/jacoco.gradle"

Here is a sample module’s build.gradle

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply from: "../common-module.gradle"
apply from: "$rootDir/jacoco.gradle"

dependencies {
implementation deps.room
}
Sample coverage report

Instrumentation Testing

Testing your Activity/Fragment classes by mocking some behaviors will make things easier.

Only injecting the mock repository is enough in our case.

@RunWith(AndroidJUnit4::class)
class MainActivitySuccessTest {

@get:Rule
val rule = ActivityTestRule(MainActivity::class.java, false, false)

private val repo: MainRepository = mock()

@Before
fun setup() {
runBlocking {
val resp = ApiResponse(success = listOf(Album("title")))
whenever(repo.getAlbums()).thenReturn(resp)

}
StandAloneContext.loadKoinModules(module {
single(override = true) { repo }
})

val intent = Intent()
rule.launchActivity(intent)
}

@Test
fun testFetchAlbums() {
onView(withText("title")).check(matches(isDisplayed()))
}

}

If you want to learn more about testing you can check my previous posts.

Here is an example app with layered modularization approach.

If you like this article you can follow me on Github.

--

--