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 :
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
Example: Home feature structure
The home feature demonstrates the typical pattern:
HomeScreen.kt : Composable function that renders UI
HomeViewModel.kt : Manages UI state and business logic
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
data/source
Purpose : Data source interfacesDefines contracts for local and remote data access: interface ArticleDataSource {
suspend fun fetchArticles (): List < Article >
}
Dependencies : Only domain/model
data/local
Purpose : Local database operations using Roomdata/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.
data/remote
Purpose : Network operations using Retrofitdata/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.
data/repository
Purpose : Implements domain gateway interfacesdata/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.
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 :
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
Why separate common and commonAndroid?
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.
Navigation module
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:
Create the module directory
mkdir -p feature/newfeature/src/main/java
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"
}
Register in settings.gradle.kts
include ( ":feature:newfeature" )
Add to app module dependencies
dependencies {
implementation (projects.feature.newfeature)
}
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