Modularization
A single-module RecipeBox project puts everything — recipe list, recipe search, camera capture, shared UI widgets, the Room database — into one :app module. That's fine at first. As the project and the team grow, modularization splits it into multiple Gradle modules with explicit boundaries between them.
A Modularized RecipeBox
:app
MainActivity, navigation, app-level DI wiring
:core
Recipe model, Room, Retrofit, shared utilities
:feature-recipes
Recipe list & detail screens
:feature-search
Recipe search screen
:feature-recipes ──┐
├──▶ :core ◀── everything depends on :core
:feature-search ──┘
:app depends on both :feature-recipes and :feature-search (and :core)
Explanation: :core holds the Recipe class and data-layer code shared by every feature. Feature modules depend on :core but not on each other — :feature-search has no reason to know :feature-recipes exists.
settings.gradle
include ':app', ':core', ':feature-recipes', ':feature-search'
Declaring Module Dependencies
// feature-recipes/build.gradle
dependencies {
implementation project(':core')
implementation 'androidx.appcompat:appcompat:1.7.0'
}
api vs. implementation
Gradle's Android plugin offers two dependency configurations that control whether a dependency "leaks" to modules further up the chain.
| Config | Visible to modules that depend on this one? | Rebuild impact when it changes |
|---|---|---|
| implementation | No — stays internal | Only this module recompiles |
| api | Yes — passed through | This module and every consumer recompiles |
// core/build.gradle
dependencies {
// Room types appear in :core's public methods, other modules need them directly
api 'androidx.room:room-runtime:2.6.1'
// Retrofit is fully wrapped inside RecipeRepositoryImpl — nothing outside :core touches it
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
}
Explanation: Preferring implementation by default keeps a module's internal dependencies from becoming other modules' problem, and keeps incremental builds fast — a change to Retrofit's version here only recompiles :core, not every feature module.
Why Bother
| Benefit | What it means for RecipeBox |
|---|---|
| Build speed | Gradle compiles unchanged modules from cache and builds independent modules in parallel |
| Enforced boundaries | :feature-search literally cannot import :feature-recipes internals — the compiler blocks it |
| Reusability | :core could be reused by a future companion app without dragging any UI code along |
Is It Worth It
Honest note: Modularization pays off on larger teams and larger apps, where build times have grown painful and different people keep stepping on each other's code in one giant module. A solo developer's RecipeBox with three screens gains nothing but extra Gradle files and ceremony from splitting it up on day one — start with one module, and split when a specific pain (slow builds, merge conflicts, unclear ownership) actually shows up.
Hands-On Practice
- Create a :core module and move the Recipe class and RecipeRepository into it.
- Add :core as an implementation dependency of :app.
- Try importing a :core-internal class from a second module using only implementation — confirm the build fails.
Splitting code into modules is a structural change; it says nothing about whether the code inside
each module is actually good. The next lesson, Code Quality & Lint,
covers tools that check that.