← Back to main index | ← Back to folder
Tip
Split app into feature modules (auth, home) + core modules (network, database, ui). Dependency flow:
feature → core, never feature → feature. Use DI to decouple.
Multi-module structure · Dependency graph · Core shared layers · Feature isolation
💻 Code Example
app/
├── feature-auth/ ─→ domain/, data/, ui/, di/
├── feature-home/ ─→ domain/, data/, ui/, di/
├── core-network/ ← shared by features
├── core-database/ ← shared by features
└── core-ui/ ← shared by features
Dependency rule: ✅ feature imports core | ❌ feature imports feature (break cycle via DI interfaces/events).
| Structure | Benefit | Cost |
|---|---|---|
| Monolithic (1 module) | Simple, fast builds | Circular deps, hard to test |
| Multi-feature | Isolates concerns, parallel build | Build complexity, more coordination |
| Over-modularized (30+ modules) | Maximum isolation | Gradle overhead, hard to navigate |
🔩 Under the Hood
When you import a module:
dependencies {
implementation(project(":feature-auth"))
}Gradle resolves to:
- feature-auth's build.gradle dependencies
- Transitively includes all its imports (core-* modules)
- If circular (auth imports home), build fails
Build cache: Gradle caches compiled modules. Only rebuild if source changes.
| A user knows | An understander also knows |
|---|---|
| "Modularize to avoid circular deps" | Circular deps break class loader. Gradle detects, fails build. Forces architectural discipline. |
| "DI to decouple features" | DI container (Hilt) lives in app module. Features define interfaces in core-* modules. App wires implementations. |
| "core-* modules never import feature-*" | Prevents tight coupling. core-network usable in any feature without knowing about auth/home. Testable in isolation. |
- Transitive dependencies: Importing :feature-auth pulls all its deps (including Retrofit, Room, etc.). Can bloat app size if not careful.
- Lint/R class clashes: Multiple modules defining custom views = R class conflicts. Use namespace in build.gradle.
- Test fixtures: Sharing test data across modules is hard. Either duplicate or create test-shared modules (expensive).
Tip
Write ADR (Architecture Decision Records) for major choices. Add KDoc with @param, @throws, @return for
public APIs. Include examples in docstrings.
KDoc · ADR · Examples · Consequences
Architecture Decision Record:
💻 Code Example
# ADR-001: Use MVI instead of MVVM
## Context
State complexity growing; multiple StateFlow sources = inconsistent UI.
## Decision
Adopt MVI (Model-View-Intent) with single reducer + StateFlow.
## Consequences
✅ Single source of truth (state history)
✅ Pure reducer (testable)
❌ More boilerplate (Intent sealed class)
## Alternatives Considered
- MVVM with multiple LiveData (rejected: hard to coordinate)
- MVP (rejected: no ViewModel lifecycle)KDoc for Public APIs:
💻 Code Example
/**
* Fetches user with automatic caching and retry.
*
* @param userId ID to fetch
* @return User (cached for 1 hour)
* @throws NotFoundException if user not found
* @throws IOException if 3 retries exhausted
*
* Example: `val user = getUser(123)`
*/
suspend fun getUser(userId: Int): User🔩 Under the Hood
IDE generates docs from KDoc when hovering over function. Parameters, return type, exceptions shown.
Tooling: dokka generates HTML documentation from KDoc. Used to generate library docs (like Kotlin std lib docs).
| A user knows | An understander also knows |
|---|---|
| "Write KDoc for public APIs" | IDE parses KDoc comments. Generates popup on hover. Extracted by dokka to HTML/PDF. |
| "Include examples in docstrings" | Examples help developers understand intent. Also used for generating tutorials/cookbooks. |
| "ADR documents decisions" | ADRs live in version control. Future devs read history to understand why (vs. what). Prevents rework. |
- Stale docs: KDoc not auto-updated when API changes. Easy to document wrong behavior. Enforce doc updates in code review.
- Over-documenting: Documenting obvious APIs (getText()) wastes effort. Document why, not what.
- ADR tooling: No standard format. Teams define their own. Can become decision graveyard if not enforced.