Skip to main content

Project Structure & Conventions

Conventions remove friction. When every project on your team uses the same folder names, Gradle catalog, and package structure, switching between codebases is painless. This page is the convention this curriculum uses.

Top-level layout

my-app/
├── app/ # :app module — wires everything
├── core/
│ ├── design/ # :core:design — theme, tokens
│ ├── ui/ # :core:ui — reusable Composables
│ └── testing/ # :core:testing — fixtures, rules
├── domain/ # :domain — pure Kotlin
├── data/ # :data — repository impls
├── feature/
│ ├── catalog/ # :feature:catalog
│ ├── detail/ # :feature:detail
│ └── checkout/ # :feature:checkout
├── build-logic/ # convention plugins (shared Gradle config)
├── gradle/
│ └── libs.versions.toml # version catalog (single source of truth)
├── build.gradle.kts # root build script
├── settings.gradle.kts # module list
└── gradle.properties # JVM args, AndroidX flags

Version catalog (libs.versions.toml)

The catalog centralizes every dependency version. Modules reference them via type-safe accessors (libs.androidx.core.ktx).

[versions]
agp = "8.7.2"
kotlin = "2.0.21"
compose-bom = "2024.10.01"
hilt = "2.52"
retrofit = "2.11.0"
moshi = "1.15.1"
room = "2.6.1"
coroutines = "1.9.0"
lifecycle = "2.8.7"
nav-compose = "2.8.4"

[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.15.0" }
androidx-activity-compose= { module = "androidx.activity:activity-compose", version = "1.9.3" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
compose-ui = { module = "androidx.compose.ui:ui" }
compose-material3 = { module = "androidx.compose.material3:material3" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
nav-compose = { module = "androidx.navigation:navigation-compose", version.ref = "nav-compose" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.27" }

A module then declares dependencies as:

dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.material3)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}

Convention plugins (build-logic/)

Repetitive build.gradle.kts blocks live in convention plugins so adding a new module is a one-liner. Typical plugins:

  • myapp.android.application — applies AGP, Hilt, Compose, Kotlin, common config
  • myapp.android.library — same for library modules
  • myapp.android.feature — adds Hilt + Compose + nav for feature modules
  • myapp.kotlin.library — pure Kotlin module config
  • myapp.android.test — instrumented test setup

A new feature module then looks like:

// :feature:catalog/build.gradle.kts
plugins {
id("myapp.android.feature")
}

android {
namespace = "com.example.feature.catalog"
}

dependencies {
implementation(projects.domain)
implementation(projects.core.ui)
implementation(projects.core.design)
}

Package naming

Within each module, packages mirror the architecture layer they live in:

com.example.feature.catalog/
├── CatalogScreen.kt // top-level composable
├── CatalogViewModel.kt
├── components/ // private composables for this screen
│ ├── ProductRow.kt
│ └── EmptyCatalog.kt
└── di/
└── CatalogModule.kt // Hilt module if needed

Domain packages organize by feature, not by layer:

com.example.domain/
├── product/
│ ├── Product.kt
│ ├── ProductRepository.kt
│ └── usecase/GetProductUseCase.kt
├── wishlist/
└── checkout/

File and class naming

ElementConventionExample
Activity*ActivityMainActivity
Fragment*FragmentLoginFragment
ViewModel*ViewModelCatalogViewModel
Composable screen*ScreenCatalogScreen
State holder*UiStateCatalogUiState
Use case*UseCaseGetCatalogUseCase
Repository interface*RepositoryProductRepository
Repository impl*RepositoryImplProductRepositoryImpl
Network DTO*DtoProductDto
Room entity*EntityProductEntity
DAO*DaoProductDao
Hilt module*ModuleNetworkModule
Test class*TestCatalogViewModelTest

Source sets

Tests live in standard locations:

src/main/kotlin/ -> production code
src/test/kotlin/ -> JVM unit tests (no device, fast)
src/androidTest/kotlin/ -> instrumented tests (run on device/emulator)
src/debug/kotlin/ -> debug-only sources (LeakCanary helpers, etc.)
src/release/kotlin/ -> release-only sources (no-op stubs)

.editorconfig

Enforce style automatically across IDEs:

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

[*.{kt,kts}]
ktlint_code_style = intellij_idea
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^

[*.{xml,yml,yaml}]
indent_size = 2

A starter checklist for new projects

  1. 01

    Init the catalog

    Create gradle/libs.versions.toml with the bundles you know you'll need.

  2. 02

    Set up convention plugins

    Build-logic with myapp.android.application + myapp.android.library + myapp.android.feature.

  3. 03

    Create the layered modules

    :app, :domain, :data, :core:design, :core:ui — even if empty for now.

  4. 04

    Wire Hilt

    @HiltAndroidApp on the Application class; one Hilt module per source.

  5. 05

    Add the quality gate

    Spotless + Detekt + Lint + Kover. Wire them into ./gradlew check.

  6. 06

    CI from day 1

    GitHub Actions running check on every PR. Block merges that don't pass.