How the Trending Feature Fetches and Caches Trending Repositories in GSYGitHubAppCompose
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:
- Query Local Storage – The repository first calls
trendingDao.getAllTrendingRepos()to retrieve any persistedTrendingEntityrows from Room. - Emit Cached Snapshot – If the database contains data, entities convert to
TrendingRepoModelobjects viaTrendingEntity.toTrendingRepoModel()and emit as aRepositoryResultmarked withDataSource.CACHE. - Refresh from Network – The repository queries
GitHubApiService.getTrendingRepos(), clears old cache entries withtrendingDao.clearTrendingRepos(), inserts fresh data, and emits a secondRepositoryResultmarked withDataSource.NETWORK.
Implementation in TrendingRepository.kt
All coordination logic lives in TrendingRepository.kt within the data module. The getTrendingRepositories() function returns a Flow<RepositoryResult<List<TrendingRepoModel>>> that consumers collect to receive sequential data updates.
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 defines the storage contract:
getAllTrendingRepos()– Retrieves all cached trending entitiesclearTrendingRepos()– Deletes existing rows before inserting fresh datainsertAll()– Persists new network responses asTrendingEntityobjects
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:
@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:
TrendingEntity.toTrendingRepoModel()– Converts database rows to display models for UI consumptionTrendingRepoModel.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 originisDbEmpty– Boolean flag signaling whether the local database was initially emptydata–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:
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:
@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:
suspend fun clearTrendingCache(trendingDao: TrendingDao) {
trendingDao.clearTrendingRepos()
}
Summary
- Cache-first architecture ensures immediate UI rendering by querying
TrendingDaobefore 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
RepositoryResultallows 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 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.
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 →