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.

ConfigVisible to modules that depend on this one?Rebuild impact when it changes
implementationNo — stays internalOnly this module recompiles
apiYes — passed throughThis 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

BenefitWhat it means for RecipeBox
Build speedGradle 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

  1. Create a :core module and move the Recipe class and RecipeRepository into it.
  2. Add :core as an implementation dependency of :app.
  3. 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.