The Compose Project Template follows Clean Architecture principles with a modular structure that separates concerns and ensures scalability. The architecture is divided into three main layers: Presentation , Domain , and Data .
Architectural layers
The project implements a unidirectional data flow architecture where each layer has a specific responsibility and depends only on layers below it.
Presentation UI components, screens, and ViewModels built with Jetpack Compose
Domain Business logic, use cases, and domain models independent of frameworks
Data Data sources, repositories, and external API integrations
Module structure
The project is organized into multiple Gradle modules to enforce separation of concerns and enable parallel builds:
CPT/
├── app/ # Application entry point
├── feature/ # Feature modules (UI layer)
│ ├── home/
│ ├── launcher/
│ └── articledetail/
├── domain/ # Business logic layer
│ ├── model/ # Business objects (Bo)
│ ├── gateway/ # Repository interfaces
│ └── useCase/ # Use case implementations
├── data/ # Data layer
│ ├── source/ # Data source interfaces
│ ├── repository/ # Repository implementations
│ ├── remote/ # API data sources
│ ├── local/ # Database data sources
│ └── session/ # Preferences data sources
├── common/ # Pure Kotlin utilities
├── commonAndroid/ # Android-specific shared code
└── navigation/ # Navigation definitions
Data flow
The architecture enforces a clear data flow pattern:
User interaction
User interacts with the UI (Composable screens)
ViewModel processing
ViewModel receives the event and calls appropriate use cases
Business logic execution
Use cases execute business logic and interact with gateways (repository interfaces)
Data retrieval
Repositories coordinate between remote and local data sources
State update
Data flows back through use cases to ViewModel, updating UI state
UI recomposition
Composables observe state changes and recompose automatically
Dependency flow
The architecture follows the Dependency Rule : dependencies only point inward toward the domain layer.
The domain layer has no Android dependencies , making business logic testable and portable.
Key architectural patterns
Repository pattern
Repositories implement gateway interfaces and coordinate between multiple data sources:
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 ())
}
Gateway (interface) pattern
The domain layer defines gateway interfaces that the data layer implements:
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 >
}
Gateways are defined in the domain layer but implemented in the data layer , inverting the dependency direction.
Use case pattern
Each use case encapsulates a single business operation:
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)
}
MVVM pattern
Feature modules use the Model-View-ViewModel pattern with Jetpack Compose:
@HiltViewModel
class HomeViewModel @Inject constructor (
private val getArticlesUseCase: GetArticlesUseCase ,
private val getFavoriteArticlesUseCase: GetFavoriteArticlesUseCase ,
) : BaseViewModel < HomeUiState >() {
override val uiState: MutableStateFlow < UiState < HomeUiState >> =
MutableStateFlow ( value = UiState ( data = HomeUiState ()))
private suspend fun getArticles () =
getArticlesUseCase (limit = 5L , offset = 0L ). collectLatest { response ->
uiState. successState { currentUiState ->
currentUiState. copy (articles = response.results)
}
}
}
Benefits of this architecture
Pure domain logic without Android dependencies
Easy mocking of interfaces for unit tests
ViewModels testable without UI framework
Clear separation of concerns
Changes isolated to specific layers
Easy to locate and modify features
Modular structure supports team collaboration
Parallel builds with Gradle modules
Easy to add new features without affecting existing code
Swap implementations without changing business logic
Support multiple data sources (API, database, cache)
Framework-independent domain layer
Never let the domain layer depend on the data or presentation layers. This violates the Dependency Rule and breaks the architecture.
Next steps
Clean architecture Learn about clean architecture principles
Modules Explore module organization
Dependency injection Understand Hilt DI setup