Practical Guide to Android Componentization and Modular Development
This article explains why monolithic Android projects suffer from long compile times, high coupling, and merge conflicts, and demonstrates how componentization—using a four‑layer architecture, unified Gradle dependencies, plugin switching, code isolation, ARouter routing, decentralized APIs, and AndroidX App Startup initialization—can solve these problems with concrete Kotlin and XML examples.
In large Android projects a single module often contains all business code, causing extremely long compilation, tight coupling between features, frequent merge conflicts, and high maintenance costs.
Componentization solves these pain points by splitting the app into independent components that can be compiled and debugged separately, reducing coupling, improving reuse, and allowing parallel development.
The recommended architecture consists of four layers from top to bottom: Base Layer (framework, utilities, third‑party libraries), Middle Layer (routing and shared services), Business Layer (feature components, each runnable as a small app), and App Layer (the shell that aggregates components). Only upper layers may depend on lower ones.
To keep dependency versions consistent across many modules, the article suggests using buildSrc with Kotlin constants. Example:
plugins { `kotlin-dsl` }
repositories { gradlePluginPortal() } object Versions {
const val COMPILE_SDK = 32
const val TARGET_SDK = 32
const val MIN_SDK = 23
const val VERSION_CODE = 1
const val VERSION_NAME = "1.0.0"
const val ANDROID_GRADLE_PLUGIN = "7.1.2"
const val KOTLIN = "1.7.20"
// ...
} object Libs {
const val APPCOMPAT = "androidx.appcompat:appcompat:${Versions.APPCOMPAT}"
const val APP_STARTUP = "androidx.startup:startup-runtime:${Versions.APP_STARTUP}"
// ...
}Modules can switch between com.android.application and com.android.library plugins using a debug flag:
object Plugins {
const val DEBUG_MODULE = false
} if (Plugins.DEBUG_MODULE) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}When a component should be compiled only at runtime (to avoid direct code references), use runtimeOnly :
dependencies {
runtimeOnly project(path: ':module-account')
}For navigation between components, the article adopts the ARouter framework. Add the dependencies:
plugins { id 'kotlin-kapt' }
kapt { arguments { arg("AROUTER_MODULE_NAME", project.getName()) } }
dependencies {
implementation "com.alibaba:arouter-api:1.5.2"
kapt "com.alibaba:arouter-compiler:1.5.2"
}Annotate activities, fragments, or service providers with @Route(path = "/account/sign_in") and navigate like:
ARouter.getInstance().build("/account/sign_in").navigation()Component communication is achieved via service interfaces that extend IProvider . Example service definition:
interface AccountService : IProvider {
val isSignIn: Boolean
fun signOut()
}Retrieve the service through ARouter:
val accountService = ARouter.getInstance().build("/account/service").navigation() as? AccountServiceTo avoid a single monolithic module-common , the article recommends decentralizing APIs: each feature has its own -api module (e.g., module-account-api ) that declares paths and interfaces, while the implementation lives in module-account . Consumers depend on both:
implementation project(path: ':module-account-api')
runtimeOnly project(path: ':module-account')Component initialization uses AndroidX App Startup . Create an Initializer subclass:
class AccountInitializer : Initializer
{
override fun create(context: Context) { /* init logic */ }
override fun dependencies() = emptyList
>>()
}Register it in the component’s AndroidManifest.xml via a provider meta‑data entry:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.account.AccountInitializer"
android:value="androidx.startup" />
</provider>The dependencies() method can return other initializer classes (obtained via Class.forName ) to enforce a specific startup order.
Finally, the article summarizes that componentization reduces compile time, improves modularity, and simplifies maintenance, while acknowledging that real‑world projects may face additional challenges such as component boundaries and UI variations.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.