How the Repository Pattern Coordinates Between Network and Database in Android

The repository pattern in gsygithubappcompose coordinates network and database interactions by emitting cached Room data first via Kotlin Flow, then fetching fresh data from the Retrofit API, updating the local database, and emitting the network result to maintain a single source of truth.

The open-source Android application carguo/gsygithubappcompose demonstrates a production-ready implementation of the repository pattern that seamlessly coordinates between remote REST APIs and local SQLite storage. By abstracting data access behind repository classes located in data/src/main/java/com/shuyu/gsygithubappcompose/data/repository, the app provides a unified interface that handles offline-first scenarios and reactive data updates automatically.

Core Architecture of the Repository Pattern

Each repository in the codebase acts as a mediator between the UI layer and multiple data sources. The architecture relies on dependency injection to provide both network and database access objects.

Data Source Abstraction

Repositories inject a GitHubApiService for network operations and corresponding Room DAOs for local persistence. In UserRepository.kt, the constructor receives both the API service and multiple DAOs:

class UserRepository @Inject constructor(
    private val apiService: GitHubApiService,
    private val userDao: UserDao,
    private val eventDao: EventDao,
    private val repoDao: RepoDao
) {
    // Repository implementation
}

This design allows the repository to query local storage immediately while simultaneously initiating network requests, ensuring the UI never waits for remote data when cached content is available.

The RepositoryResult Wrapper

All repository methods return Flow<RepositoryResult<T>>, a standardized envelope defined in RepositoryResult.kt that carries three critical pieces of information:

  • Result<T>: Contains either the successful data or the failure exception
  • DataSource: An enum distinguishing between DataSource.CACHE and DataSource.NETWORK
  • isDbEmpty: A boolean flag indicating whether the database was empty before the request initiated

This wrapper enables ViewModels to distinguish between stale cached data and fresh network updates while handling loading states consistently across the application.

Implementing Cache-First Data Flow

The repository pattern implements an offline-first strategy where data always flows from local storage first, then network. Two generic helper functions centralize this logic to eliminate code duplication.

Generic Helper Functions

Located at the bottom of UserRepository.kt, the getFromCacheAndNetwork helper orchestrates single-object fetching:

private fun <T, R> getFromCacheAndNetwork(
    cacheFlow: Flow<T?>,
    networkCall: suspend () -> R,
    cacheUpdate: suspend (R) -> Unit,
    toDomain: (T) -> R
): Flow<RepositoryResult<R>> = flow {
    val cached = cacheFlow.first()
    val empty = cached == null
    
    if (cached != null) {
        emit(RepositoryResult(Result.success(toDomain(cached)), DataSource.CACHE, empty))
    }
    
    try {
        val fresh = networkCall()
        cacheUpdate(fresh)
        emit(RepositoryResult(Result.success(fresh), DataSource.NETWORK, empty))
    } catch (e: Exception) {
        emit(RepositoryResult(Result.failure(e), DataSource.NETWORK, empty))
    }
}

This function emits cached data immediately (if present), then executes the network call, updates the database via cacheUpdate, and finally emits the fresh network result.

Handling Paginated Lists

For paginated data such as user events or repository issues, the getPaginatedFromCacheAndNetwork helper extends this pattern to handle lists. It checks the database for the first page only, emits cached items, fetches from the network, replaces the database content on page one, and emits the network result.

Real-World Implementation Examples

The repository pattern manifests differently across various features, adapting to specific caching requirements while maintaining consistent core mechanics.

User Profile Loading

In UserRepository.kt, the getUser(username) method demonstrates cache-then-network coordination for user profiles:

  1. Queries userDao.getUserByLogin and emits cached user data immediately
  2. Calls apiService.getUser to fetch enriched profile data including starred repository counts
  3. Inserts or replaces the user record in UserDao using cacheUpdate
  4. Emits the network result with DataSource.NETWORK origin

This ensures the UI displays profile information instantly while background updates refresh the data.

TrendingRepository.kt handles simple list caching for trending repositories:

  • Reads all rows from TrendingDao and emits cache via Flow
  • Calls apiService.getTrendingRepos with language and time period filters
  • Executes trendingDao.clearTrendingRepos() followed by insertAll to atomically replace stored data
  • Emits fresh network results to complete the flow

Issue Lists with Pagination

IssueRepository.kt implements sophisticated coordination for repository issues, handling both search queries and standard pagination:

  • For page one without search filters, queries issueDao.getIssues and emits cached issues
  • Calls either apiService.searchIssues or apiService.getRepositoryIssues based on query parameters
  • On successful first-page responses, clears existing issues and inserts new rows via IssueDao
  • Emits both cached (when available) and network results through the standard RepositoryResult wrapper

Benefits of This Coordination Strategy

The repository pattern implementation in gsygithubappcompose delivers several architectural advantages:

  • Single Source of Truth: Room database updates occur immediately after successful network calls, ensuring subsequent data collections start from fresh cache without redundant API requests
  • Offline-First Behavior: When devices lack connectivity, flows still emit cached data while network failures wrap errors in RepositoryResult without crashing the UI
  • Consistent UI Contracts: Standardized return types allow ViewModels to handle all repositories uniformly, processing Result, DataSource, and isDbEmpty flags through shared logic
  • Testability: Injected dependencies enable unit testing with fake API services and in-memory databases, isolating repository logic from external infrastructure

Summary

  • The repository pattern in carguo/gsygithubappcompose abstracts GitHubApiService and Room DAOs behind unified repository classes in data/src/main/java/com/shuyu/gsygithubappcompose/data/repository
  • All data flows emit as Flow<RepositoryResult<T>>, first from cache (DataSource.CACHE) then network (DataSource.NETWORK)
  • Generic helpers getFromCacheAndNetwork and getPaginatedFromCacheAndNetwork eliminate duplication while enforcing consistent cache-then-network semantics
  • Real implementations in UserRepository.kt, TrendingRepository.kt, and IssueRepository.kt demonstrate single-object, list replacement, and paginated synchronization strategies
  • The architecture provides offline-first capabilities, single source of truth guarantees, and testable data layers through dependency injection

Frequently Asked Questions

What is the single source of truth in this repository pattern?

The local Room database serves as the single source of truth. After every successful network call, repositories immediately update the corresponding DAOs (such as UserDao or TrendingDao), ensuring that all subsequent data requests—whether from network responses or future UI sessions—reference the same persisted state.

How does the repository pattern handle offline scenarios?

When devices lack connectivity, the repository still emits cached data from Room via the initial cacheFlow.first() call. The network attempt fails and emits a RepositoryResult containing the exception as a Result.failure, but the UI continues displaying cached content. This offline-first approach ensures users always see data when available locally.

What is the purpose of the RepositoryResult wrapper?

RepositoryResult standardizes communication between repositories and ViewModels by packaging three elements: the actual data or error in a Result<T>, the origin of the data (DataSource.CACHE or DataSource.NETWORK), and a flag indicating whether the database was empty before the request. This allows UI layers to distinguish between initial loads, background updates, and error states consistently across all features.

How does pagination work with the cache-first strategy?

For paginated lists like user events in UserRepository.kt, the repository only queries the database for page one, emitting cached items immediately. It then fetches the requested page from the network, but only updates the database (clearing and inserting) when receiving page one results. Subsequent pages bypass the cache query and emit directly from network, preventing stale data from mixing with fresh paginated results.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →