Project Architecture
Module Dependency Graph
app
├── feature:navigation-graph
│ ├── feature:sample
│ └── feature:core (shared by all features)
├── domain:usecase
│ ├── domain:repository (interfaces only)
│ ├── domain:model (pure data classes)
│ └── domain:core (BaseUseCase, DataState, AppError)
└── data:remote-data / data:local-data
└── data:core (BaseDataSource, BaseMapper, ExceptionMapper)
└── domain:model
Strict Dependency Rules
- Feature modules depend on:
feature:core, domain:model, domain:usecase
- Feature modules NEVER depend on:
data:*, other feature:* modules
- Domain modules are 100% pure Kotlin (NO
android.* imports)
- Data modules depend on:
domain:repository, domain:model, domain:core, data:core
feature:core is the ONLY feature module that provides shared base classes
Convention Plugins (build_logic/)
| Plugin ID |
Purpose |
Apply to |
android.application |
App module config |
:app |
android.application.compose |
Compose for app |
:app |
android.feature |
Base feature module |
feature:* |
android.feature.compose |
Feature + Compose + Hilt |
feature:* |
android.library |
Library module |
domain:*, data:* |
android.hilt |
Hilt DI setup |
modules needing DI |
android.firebase |
Firebase config |
modules using Firebase |
android.test.unit |
JUnit + MockK + Coroutine test |
all modules |
android.test.instrumentation |
AndroidJUnit4 + Compose test |
feature modules |
android.test.robolectric |
Robolectric setup |
domain/data modules |
Creating a New Feature Module
- Copy
feature/sample/ as a template
- Add to
settings.gradle.kts: include(":feature:new-feature")
- Create
build.gradle.kts:
plugins {
alias(libs.plugins.android.feature.compose)
// alias(libs.plugins.android.test.unit) // Add if tests needed
}
dependencies {
implementation(projects.domain.model)
implementation(projects.domain.usecase)
}
- Create files:
XxxViewModel.kt, XxxScreen.kt, XxxNavigation.kt
- Register
NavGraphBuilder.xxxScreen() in feature:navigation-graph
- Add
implementation(projects.feature.newFeature) in app/build.gradle.kts
Creating a New UseCase
domain/usecase/src/main/java/com/template/domain/
├── usecase/newfeature/
│ ├── GetNewFeatureUseCase.kt (interface)
│ └── GetNewFeatureUseCaseImpl.kt (implementation)
└── di/
└── NewFeatureUseCaseModule.kt (Hilt @Module with @Binds)
// Interface - extends appropriate base
interface GetXxxUseCase : BaseFlowUseCaseNoParams<Flow<PagingData<Xxx>>>
// Implementation - @Inject constructor
class GetXxxUseCaseImpl @Inject constructor(
private val repository: XxxRepository
) : GetXxxUseCase {
override fun invoke(): Flow<PagingData<Xxx>> = repository.getXxx()
}
// DI Module - @Binds interface -> impl
@Module
@InstallIn(SingletonComponent::class)
interface XxxUseCaseModule {
@Binds fun bindGetXxxUseCase(impl: GetXxxUseCaseImpl): GetXxxUseCase
}
UseCase Base Types
| Type |
When to use |
BaseSuspendUseCase<T: IParams, R> |
One-shot with params |
BaseSuspendUseCaseNoParams<R> |
One-shot without params |
BaseFlowUseCase<T: IParams, R> |
Streaming with params |
BaseFlowUseCaseNoParams<R> |
Streaming without params |
Naming Conventions
| Type |
Convention |
Example |
| ViewModel |
XxxViewModel |
SampleViewModel |
| State |
XxxViewState |
SampleViewState |
| Event |
XxxViewEvent |
SampleViewEvent |
| Effect |
XxxViewEffect |
SampleViewEffect |
| Screen |
XxxScreen / XxxScreenInternal |
SampleScreen |
| Navigation |
XxxNavigation.kt + NavGraphBuilder.xxxScreen() |
sampleScreen() |
| UseCase |
VerbNounUseCase + VerbNounUseCaseImpl |
GetSampleItemsUseCase |
| Mapper |
XxxMapper |
ItemMapper |
| Repository |
XxxRepository (interface) + XxxRepositoryImpl |
ItemRepository |
Hilt DI Organization
domain/usecase/di/ -> UseCaseModules (@Binds interface -> impl)
data/remote-data/di/ -> RepositoryModules, DataSourceModules, MapperModules
data/local-data/di/ -> DatabaseModule, DaoModule, DataStoreModule
app/di/ -> AppModule (singletons, app-level bindings)
Data Layer Strategy Pattern
BaseDataSource
├── fetchData() -> FirebaseDataSourceStrategy (Firebase Task<T> -> Flow<DataState<T>>)
├── fetchRestData() -> RetrofitDataSourceStrategy (Response<BaseDto<T>> -> Flow<DataState<T>>)
├── fetchPagingData() -> PagingDataSourceStrategy (Pager -> Flow<PagingData<T>>)
└── requestData() -> Direct sync wrapper
Error Flow
Infrastructure Exception
↓ ExceptionMapper (data:core)
AppError sealed class (domain:core)
↓ DataState.Error or caught in FlowCollectionManager
ErrorHandler.getErrorMessageResId() (feature:core)
↓
String resource ID -> Toast
Build Flavors
Three product flavors for environment configuration:
| Flavor |
Purpose |
Properties file |
dev |
Development |
env/dev.properties |
staging |
QA / Staging |
env/staging.properties |
production |
Production release |
env/production.properties |
Build variant format: assembleDevDebug, assembleStagingRelease, assembleProductionRelease