# Project Architecture

> Module structure, dependency rules, and naming conventions for the Android MVI Template. Use when creating new feature or domain modules, understanding which modules can depend on which, choosing convention plugins for build.gradle.kts, following naming conventions for ViewModels/UseCases/Repositories/Screens, or adding screens to the navigation graph.

- Skill: `thetruong1099/project-architecture` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thetruong1099/project-architecture`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thetruong1099/project-architecture/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: thetruong1099 (https://skillmd.com/u/thetruong1099)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thetruong1099/project-architecture

---


# 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

1. **Feature modules** depend on: `feature:core`, `domain:model`, `domain:usecase`
2. **Feature modules** NEVER depend on: `data:*`, other `feature:*` modules
3. **Domain modules** are 100% pure Kotlin (NO `android.*` imports)
4. **Data modules** depend on: `domain:repository`, `domain:model`, `domain:core`, `data:core`
5. **`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

1. Copy `feature/sample/` as a template
2. Add to `settings.gradle.kts`: `include(":feature:new-feature")`
3. Create `build.gradle.kts`:

```kotlin
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)
}
```

4. Create files: `XxxViewModel.kt`, `XxxScreen.kt`, `XxxNavigation.kt`
5. Register `NavGraphBuilder.xxxScreen()` in `feature:navigation-graph`
6. 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)
```

```kotlin
// 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`

