The Compose Project Template uses a multi-module architecture to enforce separation of concerns, enable parallel builds, and improve code organization. Each module has a specific responsibility and well-defined dependencies.
Module types
The project contains three types of modules:
Feature modules UI implementations for specific features
Domain modules Business logic and domain models
Data modules Data sources and repositories
Complete module structure
Here’s the full module hierarchy as defined in settings.gradle.kts:
rootProject.name = "CPT"
// Application module
include(":app")
// Shared modules
include(":common") // Pure Kotlin utilities
include(":commonAndroid") // Android-specific shared code
include(":navigation") // Navigation definitions
// Data layer modules
include(":data:local") // Room database implementation
include(":data:remote") // Retrofit API implementation
include(":data:repository") // Repository implementations
include(":data:session") // SharedPreferences implementation
include(":data:source") // Data source interfaces
// Domain layer modules
include(":domain:gateway") // Repository interfaces
include(":domain:model") // Business objects
include(":domain:useCase") // Use case implementations
// Feature layer modules
include(":feature:home") // Home screen feature
include(":feature:launcher") // Launcher/splash screen
include(":feature:articledetail") // Article detail screen
The project uses TYPESAFE_PROJECT_ACCESSORS to reference modules type-safely as projects.domain.model instead of ":domain:model".
Application module (app)
The app module is the entry point that assembles all features and handles application-level configuration.
Responsibilities
Application class with Hilt setup
Main Activity and navigation host
Application-level theme and configuration
Dependency aggregation from all feature modules
Key files
@HiltAndroidApp
class App : Application () {
override fun onCreate () {
super . onCreate ()
}
}
Dependencies
The app module depends on all feature, data, and shared modules:
dependencies {
// Feature modules
implementation(projects.feature.home)
implementation(projects.feature.launcher)
implementation(projects.feature.articledetail)
// Data modules (for Hilt module installation)
implementation(projects.data.local)
implementation(projects.data.repository)
implementation(projects.data.remote)
implementation(projects.data.source)
implementation(projects.data.session)
// Shared modules
implementation(projects.commonAndroid)
implementation(projects.navigation)
// Hilt for DI
implementation(libs.hilt.android)
implementation(libs.hilt.navigation)
ksp(libs.hilt.compiler)
}
The app module includes data modules to ensure Hilt DI modules are installed, even though feature modules don’t directly depend on them.
Domain layer modules
The domain layer is the heart of the application and contains zero Android dependencies.
domain/model
Defines business objects (Bo) used throughout the application.
Dependencies:
dependencies {
// No dependencies on other project modules
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
}
Example structure:
package es.mobiledev.domain.model.article
data class ArticleBo (
val id: Long ,
val title: String ,
val authors: List < AuthorBo >,
val url: String ,
val imageUrl: String ,
val newsSite: String ,
val summary: String ,
val publishedAt: String ,
val updatedAt: String ,
)
data class AuthorBo (
val name: String ,
val socials: SocialsBo ?,
)
The Bo suffix stands for “Business Object” and distinguishes domain models from DTOs (Data Transfer Objects) and DBOs (Database Objects).
domain/gateway
Defines repository interfaces that the data layer implements.
Dependencies:
dependencies {
implementation(projects.domain.model)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
}
Example structure:
package es.mobiledev.domain.gateway.article
interface ArticleGateway {
suspend fun getArticles (limit: Long , offset: Long ): Flow < ArticleResponseBo >
suspend fun getArticleById (id: Long ): Flow < ArticleBo >
suspend fun getFavoriteArticles (): Flow < List < ArticleBo >>
suspend fun saveFavoriteArticle (articleBo: ArticleBo )
suspend fun removeFavoriteArticle (articleBo: ArticleBo )
suspend fun isArticleFavorite (id: Long ): Flow < Boolean >
}
domain/useCase
Contains use case implementations and Hilt DI modules.
Dependencies:
dependencies {
implementation(projects.domain.model)
implementation(projects.domain.gateway)
// Hilt for dependency injection
implementation(libs.hilt.android)
implementation(libs.hilt.navigation)
ksp(libs.hilt.compiler)
}
Example structure:
package es.mobiledev.domain.usecase.article
interface GetArticlesUseCase {
suspend operator fun invoke (
limit: Long ,
offset: Long
): Flow < ArticleResponseBo >
}
class GetArticlesUseCaseImpl (
private val articleGateway: ArticleGateway ,
) : GetArticlesUseCase {
override suspend fun invoke (
limit: Long ,
offset: Long
): Flow < ArticleResponseBo > = articleGateway. getArticles (limit, offset)
}
Use case examples
Naming conventions
GetArticlesUseCase: Fetch article list
GetArticleByIdUseCase: Get article details
GetFavoriteArticlesUseCase: Get favorited articles
IsArticleFavoriteUseCase: Check favorite status
SaveOrRemoveFavoriteArticleUseCase: Toggle favorite
GetLastOpenTimeUseCase: Get last app open time
SaveLastOpenTimeUseCase: Save app open timestamp
Use case interfaces: *UseCase
Implementations: *UseCaseImpl
Verb-noun naming: Get, Save, Remove, Update
Invoke operator for clean syntax
Data layer modules
The data layer implements domain interfaces and provides data from various sources.
data/source
Defines data source interfaces (contracts for data providers).
Dependencies:
dependencies {
implementation(projects.domain.model)
implementation(libs.androidx.core.ktx)
}
Example structure:
package es.mobiledev.data.source.article
interface ArticleRemoteDataSource {
suspend fun getArticles (limit: Long , offset: Long ): ArticleResponseBo
suspend fun getArticleById (id: Long ): ArticleBo
}
interface ArticleLocalDataSource {
suspend fun saveFavoriteArticle (article: ArticleBo )
suspend fun removeFavoriteArticle (article: ArticleBo )
suspend fun getFavoriteArticles (): List < ArticleBo >
suspend fun isArticleFavorite (id: Long ): Boolean
}
data/remote
Implements remote data sources using Retrofit.
Dependencies:
dependencies {
implementation(projects.data.source)
implementation(projects.domain.model)
// Retrofit and networking
implementation(libs.retrofit)
implementation(libs.moshi)
implementation(libs.moshi.kotlin)
implementation(libs.okhttp.logging)
// Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
Structure:
data/remote/
├── article/
│ ├── ArticleRemoteDataSourceImpl.kt # Implementation
│ ├── ArticleWs.kt # Retrofit interface
│ ├── Mapper.kt # DTO to Bo conversion
│ └── dto/
│ ├── ArticleDto.kt
│ ├── ArticleResponseDto.kt
│ ├── AuthorDto.kt
│ └── SocialsDto.kt
├── di/
│ └── RemoteModule.kt # Hilt DI module
└── util/
└── RemoteConstants.kt # API constants
data/local
Implements local data sources using Room.
Dependencies:
dependencies {
implementation(projects.data.source)
implementation(projects.domain.model)
// Room database
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
// Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
Structure:
data/local/
├── article/
│ ├── dao/
│ │ ├── ArticleDao.kt
│ │ └── Mapper.kt # DBO to Bo conversion
│ ├── datasource/
│ │ └── ArticleLocalDataSourceImpl.kt
│ └── dbo/
│ └── ArticleDbo.kt # Room entity
├── di/
│ └── LocalModule.kt # Hilt DI module
├── AppRoomDatabase.kt
└── RoomConstants.kt
data/session
Implements SharedPreferences for user preferences.
Dependencies:
dependencies {
implementation(projects.data.source)
// Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
Structure:
data/session/
├── PreferencesDataSourceImpl.kt
├── di/
│ └── SessionModule.kt
└── util/
├── PrefKeys.kt
├── SessionConstants.kt
└── SessionUtils.kt
data/repository
Implements gateway interfaces and coordinates between data sources.
Dependencies:
dependencies {
implementation(projects.data.source)
implementation(projects.domain.gateway)
implementation(projects.domain.model)
// Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
Structure:
package es.mobiledev.data.repository.article
class ArticleRepository (
private val remote: ArticleRemoteDataSource ,
private val local: ArticleLocalDataSource ,
) : ArticleGateway {
override suspend fun getArticles (
limit: Long ,
offset: Long
): Flow < ArticleResponseBo > = flowOf (remote. getArticles (limit, offset))
override suspend fun getFavoriteArticles (): Flow < List < ArticleBo >> =
flowOf (local. getFavoriteArticles ())
// ... other implementations
}
Why separate data/source and implementations?
Separating interfaces (data/source) from implementations allows:
Feature modules depend on interfaces only
Swap implementations without changing consumers
Test with mock implementations easily
Follow Dependency Inversion Principle
Data transformation strategy
Each data module converts between its own models and domain models: Remote: ArticleDto → toBo() → ArticleBoLocal: ArticleBo → toDbo() → ArticleDbo (and reverse)Repository: Coordinates transformations transparently
Feature layer modules
Feature modules contain UI implementations using Jetpack Compose.
feature/home
Home screen with article list and favorites.
Dependencies:
dependencies {
// Domain layer
implementation(projects.domain.model)
implementation(projects.domain.useCase)
// Shared UI components
implementation(projects.commonAndroid)
// Compose and Android
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.material3)
// Hilt
implementation(libs.hilt.android)
implementation(libs.hilt.navigation)
ksp(libs.hilt.compiler)
}
Structure:
feature/home/
├── component/
│ ├── HomeScreenContent.kt
│ └── HomeScreenTopBar.kt
├── screen/
│ └── HomeScreen.kt
├── state/
│ └── HomeUiState.kt
└── viewmodel/
└── HomeViewModel.kt
feature/articledetail
Article detail screen with full content display.
Structure:
feature/articledetail/
├── component/
│ ├── ArticleDetailActionRow.kt
│ ├── ArticleDetailBody.kt
│ ├── ArticleDetailBottomBar.kt
│ ├── ArticleDetailImage.kt
│ └── ArticleDetailScreenContent.kt
├── screen/
│ └── ArticleDetailScreen.kt
├── state/
│ └── ArticleDetailUiState.kt
└── viewmodel/
└── ArticleDetailViewModel.kt
feature/launcher
Splash screen with app initialization logic.
Structure:
feature/launcher/
├── component/
│ └── LauncherScreenContent.kt
├── screen/
│ └── LauncherScreen.kt
├── state/
│ └── LauncherUiState.kt
└── viewmodel/
└── LauncherViewModel.kt
All feature modules follow the same structure: component/, screen/, state/, and viewmodel/ directories.
Shared modules
Shared modules contain code reused across multiple features.
common
Pure Kotlin utilities with no Android dependencies.
Dependencies:
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
}
Contents:
CommonConstants.kt: Application-wide constants
extensions/DateExtensions.kt: Date utility functions
extensions/StringExtensions.kt: String utility functions
util/AppDispatchers.kt: Coroutine dispatchers wrapper
commonAndroid
Android-specific shared code including base classes and reusable UI components.
Dependencies:
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.material3)
}
Contents:
commonAndroid/
├── ui/
│ ├── base/
│ │ ├── BaseScreen.kt
│ │ ├── BaseViewModel.kt
│ │ ├── ScreenWrapper.kt
│ │ └── UiState.kt
│ └── component/
│ ├── article/ArticleItem.kt
│ ├── button/CPTButton.kt
│ ├── navigationBar/CptNavigationBar.kt
│ ├── topBar/CptTopBar.kt
│ └── ...
└── util/
├── ComposableUtils.kt
├── IntentUtils.kt
└── TimeUtils.kt
navigation
Contains navigation-related definitions and utilities.
Dependencies:
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.navigation.runtime.ktx)
implementation(libs.androidx.navigation.compose)
}
Module dependency graph
Here’s how modules depend on each other:
Green modules represent the domain layer , which has no dependencies on outer layers.
Module guidelines
Keep domain pure
Domain modules (domain/*) must not depend on Android framework classes or data/feature modules.
Features depend on domain only
Feature modules depend on domain/useCase and domain/model, never on data modules directly.
Data implements domain
Data modules implement interfaces defined in the domain layer through dependency inversion.
One responsibility per module
Each module should have a single, well-defined purpose. Split large modules when they grow.
Minimize dependencies
Only add dependencies that are truly necessary. Fewer dependencies mean faster builds.
Benefits of modular architecture
Parallel builds Gradle can build independent modules in parallel, reducing build times significantly.
Clear boundaries Module boundaries enforce separation of concerns and prevent unwanted dependencies.
Team collaboration Different teams can work on different modules without merge conflicts.
Testability Modules can be tested independently with their own test suites.
Reusability Common modules can be extracted and reused in other projects.
Encapsulation Implementation details are hidden within modules, exposing only public APIs.
Adding a new module
To add a new feature module:
Create module directory
mkdir -p feature/myfeature/src/main/java/es/mobiledev/feature/myfeature
Add build.gradle.kts
Create feature/myfeature/build.gradle.kts with standard dependencies: plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
dependencies {
implementation(projects.domain.model)
implementation(projects.domain.useCase)
implementation(projects.commonAndroid)
// ... standard feature dependencies
}
Include in settings.gradle.kts
include(":feature:myfeature")
Add to app module
implementation(projects.feature.myfeature)
Sync Gradle
Run ./gradlew --refresh-dependencies to sync the project.
Always follow the dependency rules: features depend on domain, data implements domain, domain depends on nothing.
Next steps
Dependency injection Learn how Hilt wires modules together
Architecture overview Review high-level architecture