Skip to main content
The Compose Project Template uses a modular architecture to promote separation of concerns, reusability, and scalability. This guide explains each module’s purpose and how they work together.

Architecture overview

The project follows Clean Architecture principles with a clear separation between layers:
Modules are organized in layers where dependencies flow downward from app to features to domain. This ensures business logic remains independent of Android framework and UI.

Module breakdown

App module

Location: /app Purpose: Application entry point and dependency injection setup Key files:
app/src/main/java/es/mobiledev/cpt/App.kt
@HiltAndroidApp
class App : Application() {
    override fun onCreate() {
        super.onCreate()
    }
}
app/src/main/java/es/mobiledev/cpt/MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            navController = rememberNavController()
            navController?.let { safeNavController ->
                CPTTheme {
                    AppNavigation(navController = safeNavController)
                }
            }
        }
    }
}
Dependencies:
  • All feature modules (feature:home, feature:launcher, feature:articledetail)
  • All data modules (data:local, data:remote, data:repository, data:source, data:session)
  • Shared modules (commonAndroid, navigation)
Build configuration:
app/build.gradle.kts
android {
    namespace = AppConfig.namespace
    compileSdk = AppConfig.compileSdkVersion
    
    defaultConfig {
        applicationId = AppConfig.applicationId
        minSdk = AppConfig.minSdkVersion
        targetSdk = AppConfig.targetSdkVersion
        versionCode = AppConfig.versionCode
        versionName = AppConfig.versionName
    }
}
The app module should not contain business logic. It only wires dependencies and launches the application.

Feature modules

Location: /feature/{home, launcher, articledetail} Purpose: Self-contained UI features with their own screens, ViewModels, and UI logic Structure:
feature/home/
├── build.gradle.kts
└── src/main/java/
    └── es/mobiledev/feature/home/
        ├── HomeScreen.kt        # Composable UI
        ├── HomeViewModel.kt     # Business logic
        └── HomeUiState.kt       # UI state data class
Dependencies:
feature/home/build.gradle.kts
dependencies {
    implementation(projects.domain.model)
    implementation(projects.domain.useCase)
    implementation(projects.commonAndroid)
    
    // Compose dependencies
    implementation(libs.androidx.ui)
    implementation(libs.androidx.material3)
    
    // Hilt for dependency injection
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
}
Key characteristics:
  • Each feature is independent and can be developed in isolation
  • Features depend on domain layer for business logic
  • UI components are built with Jetpack Compose
  • ViewModels extend BaseViewModel from commonAndroid
The home feature demonstrates the typical pattern:
  1. HomeScreen.kt: Composable function that renders UI
  2. HomeViewModel.kt: Manages UI state and business logic
  3. HomeUiState.kt: Data class representing screen state
Features use Hilt for dependency injection and observe state using Compose’s collectAsState().

Domain layer

Location: /domain/{gateway, model, useCase} Purpose: Pure Kotlin business logic with no Android dependencies

domain/model

Data models and entities used across the app

domain/gateway

Repository interfaces that define data contracts

domain/useCase

Business logic operations that features invoke
domain/model (/domain/model):
// Pure Kotlin data classes
data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val imageUrl: String?
)
Build configuration:
domain/model/build.gradle.kts
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
}

dependencies {
    // Minimal dependencies - just Android basics
    implementation(libs.androidx.core.ktx)
}
domain/gateway (/domain/gateway):
// Repository interfaces
interface ArticleGateway {
    suspend fun getArticles(): List<Article>
    suspend fun getArticleById(id: Long): Article?
}
domain/useCase (/domain/useCase):
class GetArticlesUseCase @Inject constructor(
    private val gateway: ArticleGateway
) {
    suspend operator fun invoke(): List<Article> {
        return gateway.getArticles()
    }
}
Dependencies:
domain/useCase/build.gradle.kts
dependencies {
    implementation(projects.domain.model)
    implementation(projects.domain.gateway)
    
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
}
The domain layer contains zero Android framework dependencies except for minimal Android library requirements. This makes business logic easily testable.

Data layer

Location: /data/{local, remote, repository, session, source} Purpose: Data management, persistence, and network operations
1

data/source

Purpose: Data source interfacesDefines contracts for local and remote data access:
interface ArticleDataSource {
    suspend fun fetchArticles(): List<Article>
}
Dependencies: Only domain/model
2

data/local

Purpose: Local database operations using Room
data/local/build.gradle.kts
dependencies {
    implementation(projects.domain.model)
    implementation(projects.data.source)
    
    api(libs.androidx.room.runtime)
    ksp(libs.androidx.room.compiler)
    implementation(libs.androidx.room.ktx)
}
Implements Room database, DAOs, and entities.
3

data/remote

Purpose: Network operations using Retrofit
data/remote/build.gradle.kts
dependencies {
    implementation(projects.data.source)
    implementation(projects.domain.model)
    
    implementation(libs.retrofit)
    implementation(libs.converter.moshi)
    implementation(libs.moshi)
    ksp(libs.moshi.kotlin.codegen)
    implementation(libs.logging.interceptor)
}
Handles API calls, JSON parsing with Moshi, and HTTP client configuration.
4

data/repository

Purpose: Implements domain gateway interfaces
data/repository/build.gradle.kts
dependencies {
    implementation(projects.data.source)
    implementation(projects.domain.gateway)
    implementation(projects.domain.model)
    
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
}
Coordinates between local and remote data sources, implements caching strategies.
5

data/session

Purpose: User session and authentication state managementManages user tokens, login state, and session persistence.
Data flow example:

Common module

Location: /common Purpose: Shared Kotlin utilities with no Android dependencies Contains:
  • Extension functions (String, Date)
  • Coroutine dispatchers
  • Constants
  • Utility classes
Example:
common/src/main/java/es/mobiledev/common/util/AppDispatchers.kt
class AppDispatchers(
    val main: CoroutineDispatcher,
    val default: CoroutineDispatcher,
    val io: CoroutineDispatcher,
)
common/src/main/java/es/mobiledev/common/extensions/StringExtensions.kt
fun String.capitalizeWords(): String {
    return split(" ").joinToString(" ") { 
        it.replaceFirstChar { char -> char.uppercase() } 
    }
}
Build configuration:
common/build.gradle.kts
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
}

dependencies {
    implementation(libs.androidx.core.ktx)
}
The common module contains pure Kotlin code and can be used across all other modules.

CommonAndroid module

Location: /commonAndroid Purpose: Shared Android-specific UI components and base classes Contains:
  • Base ViewModel and UiState classes
  • Reusable Compose components
  • Screen wrappers
  • Custom navigation components
Example base classes:
commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/base/BaseViewModel.kt
abstract class BaseViewModel<T> : ViewModel() {
    protected abstract val uiState: MutableStateFlow<UiState<T>>
    
    fun getUiState(): StateFlow<UiState<T>> = uiState.asStateFlow()
    
    fun MutableStateFlow<UiState<T>>.updateState(block: (T) -> T) {
        update { currentUiState ->
            currentUiState.copy(data = block(currentUiState.data))
        }
    }
    
    fun MutableStateFlow<UiState<T>>.loadingState() {
        update { it.copy(isLoading = true) }
    }
    
    fun MutableStateFlow<UiState<T>>.successState(block: (T) -> T) {
        update { currentUiState ->
            currentUiState.copy(data = block(currentUiState.data), isLoading = false)
        }
    }
}
commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/base/UiState.kt
data class UiState<T>(
    val data: T,
    val isLoading: Boolean = false,
    val error: String? = null
)
Dependencies:
commonAndroid/build.gradle.kts
dependencies {
    implementation(projects.domain.model)
    implementation(projects.navigation)
    
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.material3)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.coil.compose)
}
Reusable components:
  • ArticleItem: Card component for displaying articles
  • CPTButton: Styled button component
  • CptNavigationBar: Bottom navigation bar
  • ScreenWrapper: Consistent screen layout wrapper
common: Pure Kotlin code that can run on any platform (JVM, Android, iOS with KMP)commonAndroid: Android-specific code requiring Android framework (Compose, ViewModel, etc.)This separation enables future multiplatform support.
Location: /navigation Purpose: Type-safe navigation using Kotlin Serialization Key file:
navigation/src/main/java/es/mobiledev/navigation/AppScreens.kt
@Serializable
sealed interface AppScreens {
    val module: NavigationModule
    val hasTopBar: Boolean
    val hasBottomBar: Boolean

    @Serializable
    data object Launcher : AppScreens {
        override val module = NavigationModule.LAUNCHER
        override val hasTopBar = false
        override val hasBottomBar = false
    }

    @Serializable
    data object Home : AppScreens {
        override val module = NavigationModule.HOME
        override val hasTopBar = true
        override val hasBottomBar = true
    }

    @Serializable
    data class ArticleDetail(val id: Long) : AppScreens {
        override val module = NavigationModule.ARTICLE_DETAIL
        override val hasTopBar = true
        override val hasBottomBar = true
    }
}
Dependencies:
navigation/build.gradle.kts
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlinx.serialization)
}

dependencies {
    implementation(libs.kotlinx.serialization.json)
}
Benefits:
  • Type-safe: Compile-time checking of navigation arguments
  • Serializable: Automatic serialization of navigation parameters
  • Centralized: All screens defined in one place
  • Configurable: Screen-level configuration for top/bottom bars
Navigation uses the new type-safe Navigation Compose API with Kotlin Serialization, eliminating string-based routes and navigation arguments.

BuildSrc module

Location: /buildSrc Purpose: Centralized build configuration and versioning Key file:
buildSrc/src/main/java/es/mobiledev/buildsrc/AppConfig.kt
object AppConfig {
    const val applicationId = "es.mobiledev.cpt"
    const val namespace = "es.mobiledev.cpt"
    const val applicationName = "CPT"
    const val versionCode = 1
    const val versionName = "0.0.1"
    const val compileSdkVersion = 36
    const val targetSdkVersion = 36
    const val minSdkVersion = 26
    const val testRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Build file:
buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    google()
    mavenCentral()
}
Usage across modules:
import es.mobiledev.buildsrc.AppConfig

android {
    namespace = AppConfig.namespace
    compileSdk = AppConfig.compileSdkVersion
    
    defaultConfig {
        minSdk = AppConfig.minSdkVersion
        targetSdk = AppConfig.targetSdkVersion
    }
}
Using buildSrc ensures all modules use the same SDK versions and configuration, preventing inconsistencies.

Module dependency rules

To maintain a clean architecture, follow these dependency rules:

✅ Allowed

  • Features depend on domain and commonAndroid
  • Data layer depends on domain interfaces
  • Repository implements gateway interfaces
  • All modules can depend on common utilities

❌ Prohibited

  • Domain depends on data or features
  • Features depend on other features
  • Data modules depend on features
  • Circular dependencies between any modules
Dependency flow:
app → feature → domain ← data

              common/commonAndroid

Adding a new module

To add a new feature module:
1

Create the module directory

mkdir -p feature/newfeature/src/main/java
2

Create build.gradle.kts

Copy from an existing feature module and update the namespace:
feature/newfeature/build.gradle.kts
android {
    namespace = "es.mobiledev.feature.newfeature"
}
3

Register in settings.gradle.kts

settings.gradle.kts
include(":feature:newfeature")
4

Add to app module dependencies

app/build.gradle.kts
dependencies {
    implementation(projects.feature.newfeature)
}
5

Sync project

Click File > Sync Project with Gradle Files in Android Studio.

Module organization best practices

Single responsibility

Each module should have one clear purpose and reason to change.

Minimize dependencies

Only depend on modules you actually need. Avoid transitive dependencies.

Use interfaces

Domain layer should define interfaces that data layer implements.

Test in isolation

Each module should have its own test suite and be testable independently.

Project structure summary

Compose-Project-Template/
├── app/                          # Application entry point
├── feature/
│   ├── home/                    # Home screen feature
│   ├── launcher/                # Splash/launcher feature
│   └── articledetail/           # Article detail feature
├── domain/
│   ├── model/                   # Data models
│   ├── gateway/                 # Repository interfaces
│   └── useCase/                 # Business logic operations
├── data/
│   ├── local/                   # Room database
│   ├── remote/                  # Retrofit API client
│   ├── repository/              # Gateway implementations
│   ├── source/                  # Data source interfaces
│   └── session/                 # Session management
├── common/                       # Shared Kotlin utilities
├── commonAndroid/                # Shared Android UI components
├── navigation/                   # Type-safe navigation
└── buildSrc/                     # Build configuration

Next steps

Architecture guide

Deep dive into Clean Architecture principles

Adding modules

Learn how to create new feature modules

Dependency injection

Understand Hilt setup and usage

Testing strategy

Learn how to test each module type

Build docs developers (and LLMs) love