# How Feature Modules Communicate Through the Data Layer in GSYGitHubAppCompose

> Discover how GSYGitHubAppCompose feature modules communicate via a shared data layer. Centralize caching, network requests, and state management with BaseViewModel for loose coupling.

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

---

**Feature modules in GSYGitHubAppCompose communicate exclusively through a shared data layer that exposes repository interfaces, ensuring loose coupling while centralizing caching, network requests, and state management via BaseViewModel.**

The GSYGitHubAppCompose project demonstrates a clean architecture where isolated feature modules—such as **Trending**, **Issue**, **Home**, and **RepoDetail**—never reference each other directly. Instead, they communicate through a standardized data layer that abstracts local database access and remote API calls behind repository interfaces.

## The Data Layer Architecture

The data layer resides in `/data/src/main/java/com/shuyu/gsygithubappcompose/data/` and provides the sole communication channel for all feature modules. It consists of three core components that standardize how data flows through the application.

### RepositoryResult and DataSource Abstraction

At the heart of the communication pattern sits the `RepositoryResult` wrapper class defined in [`/data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/RepositoryResult.kt`](https://github.com/carguo/gsygithubappcompose/blob/main//data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/RepositoryResult.kt). This class encapsulates the actual data payload alongside a `DataSource` enum indicating whether the data originated from `CACHE` or `NETWORK`.

```kotlin
// RepositoryResult.kt
data class RepositoryResult<T>(
    val data: Result<T>,
    val dataSource: DataSource,
    val isDbEmpty: Boolean
)

enum class DataSource {
    CACHE, NETWORK
}

```

This abstraction allows feature modules to react differently to cached versus fresh data without knowing the underlying implementation details.

### Repository Classes

Concrete repositories such as `TrendingRepository` and `IssueRepository` expose methods returning `Flow<RepositoryResult<T>>`. These classes coordinate between local DAOs and remote API services. For example, [`TrendingRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/TrendingRepository.kt) combines `TrendingDao` database access with `apiService` network calls to provide a unified data stream.

## How Feature Modules Request Data Through Repositories

Feature modules obtain data by injecting the appropriate repository into their ViewModel, which extends `BaseViewModel`. The communication follows a strict four-step flow:

1. **Feature ViewModel initiates requests** by calling repository methods that return `Flow<RepositoryResult<T>>`
2. **Repository emits cached data first** (if available), then fetches network data and emits a second result marked with `DataSource.NETWORK`
3. **BaseViewModel orchestrates state updates** using `handleResult()` to merge data, manage pagination, and toggle loading indicators
4. **UI layer observes StateFlow** updates through the `uiState` property exposed by `BaseViewModel`

This pattern ensures that every feature module communicates with the data layer identically, regardless of the specific data type being requested.

## Concrete Example: Trending Feature Communication

The Trending feature demonstrates the complete communication cycle. The `TrendingViewModel` injects `TrendingRepository` and delegates all loading logic to `BaseViewModel` methods.

```kotlin
// TrendingViewModel.kt (feature/trending)
@HiltViewModel
class TrendingViewModel @Inject constructor(
    private val trendingRepository: TrendingRepository,
    preferencesDataStore: UserPreferencesDataStore,
    private val stringResourceProvider: StringResourceProvider
) : BaseViewModel<TrendingUiState>( /* … */ ) {

    override fun loadData(initialLoad: Boolean, isRefresh: Boolean, isLoadMore: Boolean) {
        launchDataLoadWithUser(initialLoad, isRefresh, isLoadMore) { _, _ ->
            trendingRepository.getTrendingRepositories(since = "daily", languageType = null)
                .collect { repoResult ->
                    repoResult.data.fold(
                        onSuccess = { newRepos ->
                            handleResult(
                                newItems = newRepos,
                                pageToLoad = 1,
                                isRefresh = isRefresh,
                                initialLoad = initialLoad,
                                isLoadMore = false,
                                source = repoResult.dataSource,
                                isDbEmpty = repoResult.isDbEmpty,
                                updateSuccess = { state, items, _, _, _, _ ->
                                    state.copy(repositories = items, hasMore = false)
                                },
                                updateFailure = { state, _, _ ->
                                    state.copy(repositories = emptyList(), hasMore = false)
                                }
                            )
                        },
                        onFailure = { ex ->
                            updateErrorState(ex, isLoadMore,
                                stringResourceProvider.getString(R.string.error_failed_to_load_repositories))
                        }
                    )
                }
        }
    }
}

```

The `TrendingRepository` implements the dual-source emission pattern:

```kotlin
// TrendingRepository.kt (data/repository)
fun getTrendingRepositories(since: String, languageType: String?): Flow<RepositoryResult<List<TrendingRepoModel>>> = flow {
    var isDbEmpty = false
    val cached = trendingDao.getAllTrendingRepos()
    isDbEmpty = cached.isEmpty()
    if (!isDbEmpty) {
        emit(RepositoryResult(Result.success(cached.map { it.toTrendingRepoModel() }), DataSource.CACHE, isDbEmpty))
    }
    try {
        val response = apiService.getTrendingRepos(since, languageType)
        trendingDao.clearTrendingRepos()
        trendingDao.insertAll(response.map { it.toTrendingEntity() })
        emit(RepositoryResult(Result.success(response), DataSource.NETWORK, isDbEmpty))
    } catch (e: Exception) {
        emit(RepositoryResult(Result.failure(e), DataSource.NETWORK, isDbEmpty))
    }
}

```

The repository first serves cached data from `trendingDao`, then updates the database after a successful network call, and finally emits fresh data marked with `DataSource.NETWORK`.

## Cross-Module Data Sharing Without Direct Coupling

The Issue feature demonstrates how modules share contextual data without creating dependencies. `IssueViewModel` (located in [`feature/issue/src/main/java/com/shuyu/gsygithubappcompose/feature/issue/IssueViewModel.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/feature/issue/src/main/java/com/shuyu/gsygithubappcompose/feature/issue/IssueViewModel.kt)) injects both `IssueRepository` and `UserPreferencesDataStore` to perform permission checks—comparing the current username against repository owners or comment authors—without directly accessing other feature modules.

```kotlin
// IssueViewModel.kt (feature/issue)
class IssueViewModel @Inject constructor(
    private val issueRepository: IssueRepository,
    private val preferencesDataStore: UserPreferencesDataStore,
    private val stringResourceProvider: StringResourceProvider,
    savedStateHandle: SavedStateHandle
) : BaseViewModel<IssueUiState>( /* … */ ) {

    override fun loadData(initialLoad: Boolean, isRefresh: Boolean, isLoadMore: Boolean) {
        if (initialLoad || isRefresh) fetchIssueInfo(...)
        fetchIssueComments(..., isLoadMore)
    }
}

```

Because `UserPreferencesDataStore` resides in the shared data layer ([`core/common/datastore/UserPreferencesDataStore.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/common/datastore/UserPreferencesDataStore.kt)), multiple features can access user-specific settings and authentication state simultaneously while maintaining strict module boundaries.

## Summary

- **Feature modules never reference each other directly**; they communicate solely through repository interfaces exposed by the data layer.
- **BaseViewModel standardizes communication** by handling loading states, pagination logic, and error management across all features via methods like `handleResult()` and `launchDataLoadWithUser()`.
- **RepositoryResult abstracts data sources**, allowing features to differentiate between cached and network data without implementation coupling.
- **Shared data stores** like `UserPreferencesDataStore` enable cross-feature logic such as permission checks while preserving module isolation.

## Frequently Asked Questions

### How do feature modules avoid direct dependencies on each other?

Feature modules depend only on the data layer's repository interfaces and shared data stores. They inject specific repositories (such as `TrendingRepository` or `IssueRepository`) into their ViewModels rather than importing classes from other feature modules. This inversion of control ensures that the Trending module knows nothing about the Issue module's implementation, and vice versa.

### What role does BaseViewModel play in feature communication?

`BaseViewModel`—defined in [`/data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/vm/BaseViewModel.kt`](https://github.com/carguo/gsygithubappcompose/blob/main//data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/vm/BaseViewModel.kt)—provides generic machinery for data fetching and state management. It exposes a `StateFlow<UI_STATE>` that all feature UIs observe, and implements `handleResult()` to merge paginated data, toggle loading indicators, and process errors. By inheriting from `BaseViewModel`, every feature module communicates with the data layer using identical patterns.

### How does the data layer prioritize cache versus network data?

Repositories emit data sequentially through Kotlin `Flow`. They first query the local DAO and emit a `RepositoryResult` marked with `DataSource.CACHE`, then perform the network request, update the database, and emit a second result marked with `DataSource.NETWORK`. This allows the UI to display cached content immediately while refreshing in the background.

### Can feature modules share user-specific data without tight coupling?

Yes. The data layer provides `UserPreferencesDataStore` at [`core/common/datastore/UserPreferencesDataStore.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/common/datastore/UserPreferencesDataStore.kt), which exposes the current username and settings as flows. Any feature module can inject this store to perform logic—such as checking if the current user owns a repository—without importing classes from other features, maintaining strict architectural boundaries while enabling shared context.