# How the Trending Feature Fetches and Caches Trending Repositories in GSYGitHubAppCompose

> Learn how GSYGitHubAppCompose fetches and caches trending repositories using Kotlin Flow and a cache-first, network-refresh pattern. Discover the Room database and custom GitHub trending endpoint integration.

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

---

**The trending feature implements a cache-first, network-refresh pattern using Kotlin Flow, first emitting cached Room database entries via `TrendingDao`, then fetching fresh data from a custom GitHub trending endpoint and updating the cache.**

The GSYGitHubAppCompose project demonstrates modern Android architecture by separating data concerns into distinct layers. This article examines how the `TrendingRepository` orchestrates local persistence and remote API calls to deliver trending repositories with minimal latency and maximum freshness.

## The Three-Step Cache-First Architecture

The trending data flow follows a predictable **cache-first, network-refresh** sequence implemented as a cold Kotlin Flow. This approach ensures users see content immediately while guaranteeing data stays current.

The flow executes these steps in [`TrendingRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/TrendingRepository.kt):

1. **Query Local Storage** – The repository first calls `trendingDao.getAllTrendingRepos()` to retrieve any persisted `TrendingEntity` rows from Room.
2. **Emit Cached Snapshot** – If the database contains data, entities convert to `TrendingRepoModel` objects via `TrendingEntity.toTrendingRepoModel()` and emit as a `RepositoryResult` marked with `DataSource.CACHE`.
3. **Refresh from Network** – The repository queries `GitHubApiService.getTrendingRepos()`, clears old cache entries with `trendingDao.clearTrendingRepos()`, inserts fresh data, and emits a second `RepositoryResult` marked with `DataSource.NETWORK`.

## Implementation in TrendingRepository.kt

All coordination logic lives in [`TrendingRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/TrendingRepository.kt) within the data module. The `getTrendingRepositories()` function returns a `Flow<RepositoryResult<List<TrendingRepoModel>>>` that consumers collect to receive sequential data updates.

```kotlin
fun getTrendingRepositories(
    since: String,
    languageType: String?
): Flow<RepositoryResult<List<TrendingRepoModel>>> = flow {
    // 1️⃣ Load cached data immediately
    val cached = trendingDao.getAllTrendingRepos()
    val dbEmpty = cached.isEmpty()
    if (!dbEmpty) {
        emit(
            RepositoryResult(
                data = Result.success(cached.map { it.toTrendingRepoModel() }),
                dataSource = DataSource.CACHE,
                isDbEmpty = false
            )
        )
    }

    // 2️⃣ Fetch fresh data from remote service
    try {
        val response = apiService.getTrendingRepos(since, languageType)

        // 3️⃣ Atomically replace cache contents
        trendingDao.clearTrendingRepos()
        trendingDao.insertAll(response.map { it.toTrendingEntity() })

        // 4️⃣ Emit network result
        emit(
            RepositoryResult(
                data = Result.success(response),
                dataSource = DataSource.NETWORK,
                isDbEmpty = dbEmpty
            )
        )
    } catch (e: Exception) {
        emit(RepositoryResult(Result.failure(e), DataSource.NETWORK, isDbEmpty = dbEmpty))
    }
}

```

The **cold flow** behavior guarantees no database queries or network requests execute until a consumer begins collection. This prevents unnecessary resource consumption when the trending screen is not active.

## Data Layer Components

### TrendingDao.kt (Room Persistence)

The `TrendingDao` interface in [`core/database/src/main/java/com/shuyu/gsygithubappcompose/core/database/dao/TrendingDao.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/database/src/main/java/com/shuyu/gsygithubappcompose/core/database/dao/TrendingDao.kt) defines the storage contract:

- `getAllTrendingRepos()` – Retrieves all cached trending entities
- `clearTrendingRepos()` – Deletes existing rows before inserting fresh data
- `insertAll()` – Persists new network responses as `TrendingEntity` objects

### GitHubApiService.kt (Network Layer)

Remote data originates from a custom trending aggregation endpoint rather than the standard GitHub API. The `GitHubApiService` defines the Retrofit call at [`core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/api/GitHubApiService.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/api/GitHubApiService.kt):

```kotlin
@GET("https://guoshuyu.cn/github/trend/list")
suspend fun getTrendingRepos(
    @Query("since") since: String,
    @Query("languageType") languageType: String?
): List<TrendingRepoModel>

```

### DataMappers.kt (Type Conversion)

Bidirectional mapping between network models and database entities occurs in [`data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/mapper/DataMappers.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/mapper/DataMappers.kt):

- `TrendingEntity.toTrendingRepoModel()` – Converts database rows to display models for UI consumption
- `TrendingRepoModel.toTrendingEntity()` – Transforms API responses into storable entities

### RepositoryResult.kt (Source Tracking)

The `RepositoryResult` wrapper class tracks metadata about each emission:

- `dataSource` – Enum indicating **CACHE** or **NETWORK** origin
- `isDbEmpty` – Boolean flag signaling whether the local database was initially empty
- `data` – `Result<T>` containing the actual payload or exception

This metadata enables the UI layer to render appropriate loading indicators or error states based on data provenance.

## Consuming the Trending Flow

### ViewModel Collection Pattern

The UI layer typically consumes the trending flow within a `ViewModel` using `viewModelScope`:

```kotlin
class TrendingViewModel @Inject constructor(
    private val trendingRepository: TrendingRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<TrendingUiState>(TrendingUiState.Loading)
    val uiState: StateFlow<TrendingUiState> = _uiState

    fun loadTrending(since: String = "daily", language: String? = null) {
        viewModelScope.launch {
            trendingRepository.getTrendingRepositories(since, language)
                .collect { result ->
                    when (result.dataSource) {
                        DataSource.CACHE -> {
                            // Show cached data immediately
                            _uiState.value = TrendingUiState.Success(result.data.getOrThrow())
                        }
                        DataSource.NETWORK -> {
                            // Update with fresh data
                            _uiState.value = TrendingUiState.Success(result.data.getOrThrow())
                        }
                    }
                }
        }
    }
}

```

### Compose UI Integration

The composable screen observes the `StateFlow` and reacts to emissions:

```kotlin
@Composable
fun TrendingScreen(viewModel: TrendingViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()

    when (uiState) {
        is TrendingUiState.Loading -> CircularProgressIndicator()
        is TrendingUiState.Success -> TrendingList((uiState as TrendingUiState.Success).repos)
        is TrendingUiState.Error -> Text("Error loading trending repos")
    }
}

```

### Manual Cache Management

For scenarios requiring explicit cache invalidation, developers can access the DAO directly:

```kotlin
suspend fun clearTrendingCache(trendingDao: TrendingDao) {
    trendingDao.clearTrendingRepos()
}

```

## Summary

- **Cache-first architecture** ensures immediate UI rendering by querying `TrendingDao` before network requests
- **Dual emission strategy** provides both cached and network data through a single Kotlin Flow collection
- **Atomic cache replacement** clears old entries via `clearTrendingRepos()` before inserting fresh data to prevent stale content accumulation
- **Source transparency** via `RepositoryResult` allows the UI to distinguish between cached and live data for appropriate visual feedback
- **Cold flow implementation** prevents unnecessary database and network operations when no observers are active

## Frequently Asked Questions

### How does the trending feature handle network failures?

When `GitHubApiService.getTrendingRepos()` throws an exception, the flow catches the error and emits a `RepositoryResult` containing the failure in the `Result.failure()` wrapper with `DataSource.NETWORK` specified. If cached data was already emitted, the user retains stale content; if the database was empty, the UI receives an empty error state signified by `isDbEmpty = true`.

### Why does the repository use a custom endpoint instead of GitHub's official API?

The `https://guoshuyu.cn/github/trend/list` endpoint aggregates and processes GitHub's trending data independently, providing a curated list that handles rate limiting and trending calculation logic server-side. This approach reduces client complexity and avoids GitHub API authentication requirements for public trending data.

### Can I modify the cache expiration behavior?

The current implementation in [`TrendingRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/TrendingRepository.kt) does not implement time-based expiration; it performs a complete replacement on every network fetch. To add expiration logic, you would need to store timestamps in `TrendingEntity` and modify the repository to conditionally skip network requests based on age calculations before calling `getTrendingRepos()`.

### What is the benefit of using Kotlin Flow instead of LiveData for this feature?

Kotlin Flow provides superior compositional capabilities and lifecycle awareness through coroutines. The cold flow nature ensures database queries only execute during active collection, and the sequential emission pattern (cache then network) maps naturally to Flow's reactive stream model, whereas LiveData would require manual orchestration to achieve the same dual-emission behavior.