# How Room Database Implements Caching for Offline Support in gsygithubappcompose

> Discover how gsygithubappcompose uses Room database caching with a read-through pattern for seamless offline Android app support. Learn about local emission and network refresh.

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

---

**The gsygithubappcompose Android app uses Room with a read-through cache pattern that first emits local database results marked as `DataSource.CACHE`, then refreshes from the network and updates the SQLite tables atomically to provide seamless offline support.**

The `gsygithubappcompose` repository demonstrates a production-grade offline-first architecture using Android Room as a local persistence layer. By implementing a read-through caching strategy, the app ensures users can view GitHub data instantly from local storage while maintaining synchronization with remote APIs when connectivity is available.

## Room Database Architecture and Setup

The caching infrastructure begins with dependency injection in [`DatabaseModule.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/DatabaseModule.kt). The module provides a singleton `AppDatabase` using `Room.databaseBuilder()`, configured with `fallbackToDestructiveMigration(true)` to handle schema changes gracefully by clearing the cache rather than crashing legacy installations.

```kotlin
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides
    @Singleton
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "github_app.db"
        )
        .fallbackToDestructiveMigration(true)
        .build()
    }
}

```

`AppDatabase` declares the full schema including `RepositoryEntity`, `CommitEntity`, and `UserEntity`, and exposes DAO getters for each domain area. This centralized database class serves as the single source of truth for all local persistence operations.

## Entity Schema and DAO Layer with Flow

Each entity represents a cacheable domain object stored in SQLite. The Data Access Objects (DAOs) leverage Kotlin `Flow` to provide reactive streams of cached data. In [`RepositoryDao.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/RepositoryDao.kt), the `getTrendingRepositories()` method returns a `Flow<List<RepositoryEntity>>`, allowing the UI layer to observe database changes automatically.

```kotlin
@Dao
interface RepositoryDao {
    @Query("SELECT * FROM repositories WHERE is_trending = 1")
    fun getTrendingRepositories(): Flow<List<RepositoryEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertRepositories(repositories: List<RepositoryEntity>)

    @Query("DELETE FROM repositories WHERE is_trending = 1")
    suspend fun clearTrendingRepositories()

    @Transaction
    suspend fun clearAndInsert(repositories: List<RepositoryEntity>) {
        clearTrendingRepositories()
        insertRepositories(repositories)
    }
}

```

The `OnConflictStrategy.REPLACE` annotation ensures that duplicate entries are updated rather than causing insert failures, while the `clearAndInsert` method uses `@Transaction` to guarantee atomic cache refreshes—either the entire operation succeeds or the database remains unchanged.

## Read-Through Cache Pattern Implementation

The core offline logic resides in repository classes such as [`RepositoryRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/RepositoryRepository.kt). This implements a **read-through cache** where the local Room database is treated as the primary data source, with network calls serving as refresh operations.

The pattern executes in three phases:

1. **Cache First**: Query the DAO and immediately emit cached results with `DataSource.CACHE` flag
2. **Network Refresh**: Fetch fresh data from the GitHub API
3. **Cache Update**: Write the new data to Room and emit updated results with `DataSource.NETWORK`

```kotlin
fun getTrendingRepositories(language: String? = null, page: Int = 1) = flow {
    // Phase 1: Emit cached data immediately for offline support
    val cached = repositoryDao.getTrendingRepositories().first()
    if (cached.isNotEmpty()) {
        emit(RepositoryResult(
            Result.success(cached.map { it.toRepository() }),
            DataSource.CACHE,
            isDbEmpty = false
        ))
    }

    // Phase 2: Fetch from network
    val query = buildQuery(language)
    val response = apiService.searchRepositories(query, page)

    // Phase 3: Update cache atomically on first page
    if (page == 1) {
        repositoryDao.clearAndInsert(
            response.items.map { it.toEntity() }
        )
    }

    emit(RepositoryResult(
        Result.success(response.items),
        DataSource.NETWORK,
        isDbEmpty = cached.isEmpty()
    ))
}

```

This approach ensures the UI never waits for network I/O when valid cache exists, while maintaining data freshness through background synchronization.

## Transactional Cache Updates

Room's `@Transaction` annotation plays a critical role in maintaining cache integrity. The `clearAndInsert` pattern prevents partial data states during updates—critical for offline reliability where users may close the app mid-sync.

```kotlin
@Dao
interface CommitDao {
    @Query("SELECT * FROM commits WHERE owner = :owner AND repo = :repo")
    fun getRepoCommits(owner: String, repo: String): Flow<List<CommitEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertCommits(commits: List<CommitEntity>)

    @Query("DELETE FROM commits WHERE owner = :owner AND repo = :repo")
    suspend fun deleteByOwnerAndRepo(owner: String, repo: String)

    @Transaction
    suspend fun clearAndInsertRepoCommits(
        owner: String, 
        repo: String, 
        commits: List<CommitEntity>
    ) {
        deleteByOwnerAndRepo(owner, repo)
        insertCommits(commits)
    }
}

```

The `fallbackToDestructiveMigration(true)` configuration in [`DatabaseModule.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/DatabaseModule.kt) complements this strategy by ensuring that schema version mismatches result in a clean cache wipe rather than application crashes, treating the local database as a disposable cache rather than critical persistent storage.

## Summary

- **Room** serves as the local persistence layer in `gsygithubappcompose`, configured as a singleton via Hilt in [`DatabaseModule.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/DatabaseModule.kt)
- **Reactive caching** uses DAO methods returning `Flow<List<Entity>>`, enabling automatic UI updates when cache changes
- **Read-through pattern** prioritizes local data availability by emitting `DataSource.CACHE` results before network requests complete
- **Atomic updates** via `@Transaction` ensure cache consistency during refresh operations using `clearAndInsert` methods
- **Conflict resolution** uses `OnConflictStrategy.REPLACE` to handle duplicate entries during cache synchronization
- **Schema evolution** employs `fallbackToDestructiveMigration(true)` to gracefully handle database version changes by clearing stale cache data

## Frequently Asked Questions

### How does the app handle offline scenarios when no network is available?

When offline, the repository classes emit cached data immediately from Room via `Flow.first()`, marked with `DataSource.CACHE`. The network request fails silently or throws an exception caught by the calling ViewModel, but the user retains access to previously fetched data stored in the local SQLite database.

### What happens when the database schema changes in an update?

The `AppDatabase` configuration in [`DatabaseModule.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/DatabaseModule.kt) sets `fallbackToDestructiveMigration(true)`, which destroys and recreates the database when migration paths are unavailable. This is acceptable for a cache layer because the data can be re-fetched from the GitHub API, though it requires users to reload content after app updates.

### Why does the repository use `clearAndInsert` instead of upserting individual items?

The `clearAndInsert` pattern wrapped in `@Transaction` ensures atomic cache replacement. This prevents scenarios where stale data persists if the insert operation partially fails or if items were deleted from the remote source. It guarantees that the local cache exactly mirrors the API response for that specific query page.

### How does the UI distinguish between cached and fresh data?

The repository emits a `RepositoryResult` wrapper containing a `DataSource` enum indicating `CACHE` or `NETWORK`. The UI layer observes this field to display subtle indicators (such as timestamp labels or refresh animations) informing users whether they are viewing potentially stale offline data or live synchronized content.