androidengineers.Book a session

App Modularization

Exercise: Split a Monolith into 4 Modules

exercise60 minHard

This exercise takes a single-module "news app" and migrates it to a 4-module architecture: :app, :feature-feed, :data-articles, and :core-common. Follow each step, verify the build compiles, then proceed.

Starting State

:app (monolith)
├── com.example.MainActivity
├── com.example.ui.FeedFragment
├── com.example.ui.ArticleDetailFragment
├── com.example.domain.Article
├── com.example.domain.ArticleRepository
├── com.example.data.ArticleRepositoryImpl
├── com.example.data.ArticleDao
├── com.example.data.ArticleApi
└── com.example.utils.DateUtils

Step 1: Create Module Directory Structure

# From project root
mkdir -p feature-feed/src/main/java/com/example/feed
mkdir -p data-articles/src/main/java/com/example/data
mkdir -p core-common/src/main/java/com/example/core

Create build.gradle.kts for each:

// feature-feed/build.gradle.kts
plugins {
    id("com.android.library")
    id("org.jetbrains.kotlin.android")
}
android { compileSdk = 34; namespace = "com.example.feed" }
dependencies {
    implementation(project(":data-articles"))
    implementation(project(":core-common"))
    implementation("androidx.fragment:fragment-ktx:1.6.2")
    implementation("androidx.recyclerview:recyclerview:1.3.2")
}
// data-articles/build.gradle.kts
plugins { id("com.android.library"); id("org.jetbrains.kotlin.android") }
android { compileSdk = 34; namespace = "com.example.data" }
dependencies {
    implementation(project(":core-common"))
    implementation("androidx.room:room-runtime:2.6.1")
    ksp("androidx.room:room-compiler:2.6.1")
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
}
// core-common/build.gradle.kts
plugins { id("com.android.library"); id("org.jetbrains.kotlin.android") }
android { compileSdk = 34; namespace = "com.example.core" }
dependencies { /* only external libs, no internal modules */ }

Register in settings.gradle.kts:

include(":app", ":feature-feed", ":data-articles", ":core-common")

Step 2: Move Code

:core-common — move first (no dependencies on other internal modules):

mv app/src/main/java/com/example/domain/Article.kt core-common/src/main/java/com/example/core/
mv app/src/main/java/com/example/utils/DateUtils.kt core-common/src/main/java/com/example/core/

:data-articles — depends on :core-common:

mv app/src/main/java/com/example/domain/ArticleRepository.kt data-articles/src/.../
mv app/src/main/java/com/example/data/ArticleRepositoryImpl.kt data-articles/.../
mv app/src/main/java/com/example/data/ArticleDao.kt data-articles/.../
mv app/src/main/java/com/example/data/ArticleApi.kt data-articles/.../

:feature-feed — depends on :data-articles and :core-common:

mv app/src/main/java/com/example/ui/FeedFragment.kt feature-feed/.../
mv app/src/main/java/com/example/ui/ArticleDetailFragment.kt feature-feed/.../

:app — only keeps MainActivity and DI wiring.

Step 3: Fix Package Names

Update imports throughout moved files. Kotlin files moving between modules often only need their package declaration updated — the class names stay the same.

# Find broken imports after move
./gradlew :feature-feed:compileDebugKotlin 2>&1 | grep "error:"

Step 4: Verify No Cycles

./gradlew :app:dependencies --configuration debugRuntimeClasspath | grep "project"

Expected:

+--- project :feature-feed
|    +--- project :data-articles
|    |    \--- project :core-common
|    \--- project :core-common (*)
+--- project :data-articles (*)
\--- project :core-common (*)

Step 5: Measure Build Speed

# Clean build before
./gradlew clean && time ./gradlew assembleDebug

# Now change only FeedFragment.kt (in :feature-feed) and rebuild
# Only :feature-feed and :app should rebuild
time ./gradlew assembleDebug

Expected improvement: full rebuild is similar; incremental rebuild of :feature-feed only is significantly faster.

Step 6: Move Tests

Unit tests move with the code they test:

mv app/src/test/java/com/example/data/ data-articles/src/test/...
mv app/src/test/java/com/example/ui/ feature-feed/src/test/...
# Run tests per module
./gradlew :data-articles:test
./gradlew :feature-feed:test

Common Pitfalls

ProblemFix
internal access from another moduleChange to public; or move caller inside the module
R class not foundAdd resourcePrefix in module; or add module to :app's classpath
Duplicate resource namesPrefix resources: feed_item_title.xml vs app_item_title.xml
Navigation between featuresImplement navigation interface in :app (see multi-module navigation lesson)
DI @InstallIn across modulesUse Hilt's @EntryPoint for interfaces that cross module boundaries

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Exercise: Split a Monolith into 4 Modules | Android System Design | Android Engineers