# GSYGithubAppCompose Clean Architecture Folder Structure and Module Organization

> Explore the GSYGithubAppCompose Clean Architecture folder structure and module organization. Learn how to separate Presentation, Domain, and Infrastructure layers for better code maintainability.

- Repository: [Shuyu Guo/gsygithubappcompose](https://github.com/carguo/gsygithubappcompose)
- Tags: architecture
- Published: 2026-02-26

---

**GSYGithubAppCompose implements Clean Architecture through a modular Gradle project structure separating concerns into Presentation (`feature/*`), Domain (`data`), and Infrastructure (`core/*`) layers with strict unidirectional dependencies.**

This open-source GitHub client application demonstrates how to scale Android projects using Jetpack Compose by enforcing clear boundaries between UI, business logic, and data access. The Clean Architecture folder structure ensures that feature modules remain independent while sharing common infrastructure through the `core` layer.

## The Three-Layer Architectural Division

The repository organizes code into three logical layers mapped to distinct Gradle modules. This separation prevents tight coupling and enables independent testing of business logic without Android framework dependencies.

### Presentation Layer: Feature Modules

The `feature/` directory contains 14 independent modules handling UI rendering and user interaction. Each feature module owns its own screens, ViewModels, and UI state definitions.

```text
feature/
├── welcome/
├── login/
├── home/
├── dynamic/
├── trending/
├── profile/
├── search/
├── detail/
├── code/
├── issue/
├── push/
├── list/
├── notification/
└── info/

```

Each feature follows the same internal structure containing `*Screen.kt` composables, `*ViewModel.kt` classes extending `BaseViewModel`, and `*UiState.kt` data classes. For example, [`feature/issue/src/main/java/com/shuyu/gsygithubappcompose/feature/issue/IssueScreen.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/feature/issue/src/main/java/com/shuyu/gsygithubappcompose/feature/issue/IssueScreen.kt) implements the issue tracking UI while delegating business logic to the domain layer.

### Domain Layer: Business Logic Coordination

The `data/` module serves as the domain layer, coordinating business rules and data mapping between presentation and infrastructure. Located at the project root, this module contains repository implementations that abstract data sources.

Key contents include:

- `data/repository/` - Repository implementations like [`IssueRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/IssueRepository.kt) that expose clean APIs to ViewModels
- `data/mapper/` - Data transformation logic converting network entities to domain models
- [`data/repository/vm/BaseViewModel.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/data/repository/vm/BaseViewModel.kt) - Abstract ViewModel providing common state management across all features

This layer depends only on `core/` modules for low-level data access, maintaining the dependency rule that inner layers know nothing about outer layers.

### Infrastructure Layer: Core Services

The `core/` directory houses five infrastructure modules providing low-level technical capabilities:

```text
core/
├── network/          # Retrofit/Apollo services, API models (User.kt, Repository.kt)

├── database/         # Room entities, DAOs (UserEntity.kt, RepositoryDao.kt)

├── common/           # DataStore preferences, utilities (UserPreferencesDataStore.kt)

└── ui/               # Reusable Compose components, theme resources, Navigation.kt

```

The `core/network/` module defines [`GitHubApiService.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/GitHubApiService.kt) for remote data access, while `core/database/` handles local persistence. The `core/ui/` module contains shared navigation logic in [`Navigation.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/Navigation.kt) and reusable widgets used across multiple features.

## Module Dependency Rules

GSYGithubAppCompose enforces strict dependency constraints preventing circular references between layers:

```text
app          → feature/*, core/ui, data
feature/*    → data, core/ui, core/common
data         → core/network, core/database, core/common
core/ui      → core/common
core/network → independent
core/database→ independent
core/common  → independent

```

As shown in the graph, `feature/*` modules never communicate directly with `core/network` or `core/database`. All data requests flow through the `data/` module's repositories, ensuring that network implementation details remain isolated from UI code.

## Data Flow Implementation

The architecture implements unidirectional data flow using Kotlin Flow and StateFlow:

1. **Presentation** - Compose screens in `feature/*` emit UI events (clicks, scrolls) and observe `StateFlow` from ViewModels
2. **Domain** - ViewModels (e.g., `IssueViewModel`) receive events, apply business rules, and call repository methods defined in `data/repository/`
3. **Infrastructure** - Repositories fetch from `core/network/` (Retrofit/Apollo) or `core/database/` (Room) based on data requirements
4. **Response** - Results flow back through repositories → ViewModels → UI state updates, triggering recompositions in Compose screens

This chain follows the pattern: *Screen → ViewModel → Repository → Network/Database*.

## Adding a New Feature Module: Practical Example

Creating a new `feature/settings` module requires conforming to the established Clean Architecture conventions. Below is the minimal implementation skeleton.

The screen composable resides in [`feature/settings/src/main/java/com/shuyu/gsygithubappcompose/feature/settings/SettingsScreen.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/feature/settings/src/main/java/com/shuyu/gsygithubappcompose/feature/settings/SettingsScreen.kt):

```kotlin
package com.shuyu.gsygithubappcompose.feature.settings

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun SettingsScreen(
    viewModel: SettingsViewModel = hiltViewModel()
) {
    Text(text = "App Settings")
}

```

The ViewModel extends `BaseViewModel` and receives dependencies through constructor injection in [`SettingsViewModel.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/SettingsViewModel.kt):

```kotlin
package com.shuyu.gsygithubappcompose.feature.settings

import com.shuyu.gsygithubappcompose.data.repository.vm.BaseViewModel
import com.shuyu.gsygithubappcompose.core.common.datastore.UserPreferencesDataStore
import com.shuyu.gsygithubappcompose.core.common.util.StringResourceProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class SettingsViewModel @Inject constructor(
    preferencesDataStore: UserPreferencesDataStore,
    stringResourceProvider: StringResourceProvider,
) : BaseViewModel<SettingsUiState>(
    initialUiState = SettingsUiState(),
    preferencesDataStore = preferencesDataStore,
    stringResourceProvider = stringResourceProvider,
    commonStateUpdater = { state, _, _, _, _, _, _, _ -> state }
) {
    // Settings-specific logic (toggle theme, clear cache)
}

```

The UI state definition in [`SettingsUiState.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/SettingsUiState.kt) implements `BaseUiState`:

```kotlin
package com.shuyu.gsygithubappcompose.feature.settings

import com.shuyu.gsygithubappcompose.data.repository.vm.BaseUiState

data class SettingsUiState(
    val isDarkTheme: Boolean = false,
    override val isPageLoading: Boolean = false,
    override val isRefreshing: Boolean = false,
    override val isLoadingMore: Boolean = false,
    override val error: String? = null,
    override val currentPage: Int = 1,
    override val hasMore: Boolean = false,
    override val loadMoreError: Boolean = false
) : BaseUiState

```

**Integration steps:**

1. Create `feature/settings/build.gradle.kts` depending on `:data` and `:core:ui` modules
2. Register the navigation route in [`core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/Navigation.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/Navigation.kt)
3. Hilt automatically injects `SettingsViewModel` following the existing DI pattern

## Key Source Files and Their Roles

| Path | Role |
|------|------|
| [`app/src/main/java/com/shuyu/gsygithubappcompose/App.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/app/src/main/java/com/shuyu/gsygithubappcompose/App.kt) | Application class, Hilt entry point |
| [`core/ui/Navigation.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/ui/Navigation.kt) | Central Compose navigation host |
| [`core/network/GitHubApiService.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/network/GitHubApiService.kt) | Retrofit API definitions |
| [`core/database/RepositoryDao.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/database/RepositoryDao.kt) | Room data access objects |
| [`core/common/datastore/UserPreferencesDataStore.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/common/datastore/UserPreferencesDataStore.kt) | Preference storage using DataStore |
| [`data/repository/IssueRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/data/repository/IssueRepository.kt) | Business logic coordination |
| [`data/mapper/DataMappers.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/data/mapper/DataMappers.kt) | Entity-to-model transformations |
| [`data/repository/vm/BaseViewModel.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/data/repository/vm/BaseViewModel.kt) | Shared ViewModel functionality |

## Summary

- GSYGithubAppCompose uses a **modular Clean Architecture** with three distinct layers: Presentation (`feature/*`), Domain (`data`), and Infrastructure (`core/*`)
- **Strict dependency rules** prevent feature modules from accessing network or database implementations directly, enforcing repository patterns
- **Unidirectional data flow** moves from UI events through ViewModels to repositories, returning via StateFlow updates
- **Hilt dependency injection** wires components across module boundaries while maintaining testability
- **Base classes** (`BaseViewModel`, `BaseUiState`) standardize state management across all 14 feature modules

## Frequently Asked Questions

### How does the Clean Architecture folder structure improve testability in GSYGithubAppCompose?

The modular separation allows unit testing of ViewModels in `data/` without Android framework dependencies, while repository implementations can be tested against mocked `core/network` services. Since `feature/*` modules depend only on abstractions in `data/`, UI tests can use fake repositories without hitting actual APIs.

### Can feature modules communicate directly with core/database or core/network?

No. According to the dependency rules defined in the project documentation, `feature/*` modules may only depend on `data`, `core/ui`, and `core/common`. All data access must flow through repository interfaces in the `data/` module, ensuring that database schema changes or network endpoint updates don't require modifications to UI code.

### What is the purpose of the BaseViewModel class in the data module?

`BaseViewModel` (located at [`data/repository/vm/BaseViewModel.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/data/repository/vm/BaseViewModel.kt)) provides common functionality for all feature ViewModels, including state initialization, preference management via `UserPreferencesDataStore`, and standardized loading state handling. Feature-specific ViewModels extend this class to inherit pagination, error handling, and lifecycle-aware state management.

### How do I add a new screen to the existing navigation structure?

New screens must be registered in [`core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/Navigation.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/Navigation.kt), which serves as the central navigation host. The feature module containing your screen should depend on `:core:ui`, and you must use `hiltViewModel()` to obtain ViewModel instances to ensure proper dependency injection across module boundaries.