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
| Problem | Fix |
|---|---|
internal access from another module | Change to public; or move caller inside the module |
R class not found | Add resourcePrefix in module; or add module to :app's classpath |
| Duplicate resource names | Prefix resources: feed_item_title.xml vs app_item_title.xml |
| Navigation between features | Implement navigation interface in :app (see multi-module navigation lesson) |
DI @InstallIn across modules | Use Hilt's @EntryPoint for interfaces that cross module boundaries |