GSYGithubAppCompose Clean Architecture Folder Structure and Module Organization
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.
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 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 likeIssueRepository.ktthat expose clean APIs to ViewModelsdata/mapper/- Data transformation logic converting network entities to domain modelsdata/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:
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 for remote data access, while core/database/ handles local persistence. The core/ui/ module contains shared navigation logic in Navigation.kt and reusable widgets used across multiple features.
Module Dependency Rules
GSYGithubAppCompose enforces strict dependency constraints preventing circular references between layers:
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:
- Presentation - Compose screens in
feature/*emit UI events (clicks, scrolls) and observeStateFlowfrom ViewModels - Domain - ViewModels (e.g.,
IssueViewModel) receive events, apply business rules, and call repository methods defined indata/repository/ - Infrastructure - Repositories fetch from
core/network/(Retrofit/Apollo) orcore/database/(Room) based on data requirements - 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:
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:
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 implements BaseUiState:
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:
- Create
feature/settings/build.gradle.ktsdepending on:dataand:core:uimodules - Register the navigation route in
core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/Navigation.kt - Hilt automatically injects
SettingsViewModelfollowing the existing DI pattern
Key Source Files and Their Roles
| Path | Role |
|---|---|
app/src/main/java/com/shuyu/gsygithubappcompose/App.kt |
Application class, Hilt entry point |
core/ui/Navigation.kt |
Central Compose navigation host |
core/network/GitHubApiService.kt |
Retrofit API definitions |
core/database/RepositoryDao.kt |
Room data access objects |
core/common/datastore/UserPreferencesDataStore.kt |
Preference storage using DataStore |
data/repository/IssueRepository.kt |
Business logic coordination |
data/mapper/DataMappers.kt |
Entity-to-model transformations |
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) 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, 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →