DEV Community

Cover image for Building a Remote-Only Android Repository Without Local Persistence
Uray Febri
Uray Febri

Posted on Originally published at raylabs.app

Building a Remote-Only Android Repository Without Local Persistence

A working view of Building a Remote-Only Android Repository Without Local Persistence: A phone and laptop connected to a simple remote data workspace.

When designing data layers in modern mobile applications, the default pattern often leans toward a complex Chat App Architecture Message Delivery Storage featuring local caching, offline synchronization, and multi-source mediators. Many applications pull remote data from an API, store it in a local SQLite database via Room, and then expose the database as the single source of truth to the UI. While this approach is robust for offline-first scenarios, it introduces significant complexity. Developers must handle cache invalidation, database migrations, synchronization conflicts, and potential consistency bugs between the network and disk storage.

The primary architectural question is whether local persistence is genuinely required for every single feature or if it adds unnecessary overhead. If a feature deals with ephemeral data, real-time dashboards, or highly volatile search results that must always reflect current server state, a local cache can cause more problems than it solves. Stale data displayed to users can lead to confusion, and maintaining synchronization logic consumes valuable engineering time. Understanding when to bypass local storage allows you to streamline your data layer, reduce disk I/O, and focus entirely on lifecycle safety and reactive state management.

Evaluating the Need for Local Persistence

Before adding a local database to an Android feature, you need to evaluate the lifecycle and volatility of the data. Ephemeral data streams, user profile settings fetched at session start, or transactional screens often have no business logic requirement for offline storage. If the user loses network connectivity, an error state is expected rather than serving outdated records from an unmaintained cache. Bypassing the database means your repository communicates directly with the remote data source, mapping raw API responses into domain models and exposing them upward.

Removing the local database simplifies dependency injection graphs, reduces the number of abstraction layers, and eliminates schema migration overhead. However, this architectural shortcut shifts the responsibility of handling state, loading indicators, and error boundaries entirely to the memory layer. Without a local cache to fall back on, your application relies heavily on proper coroutine scopes, robust error handling, and clean reactive streams to ensure the user interface remains responsive and stable during network interruptions.

Designing Lifecycle-Safe Kotlin Flows

When a repository lacks a local database, the flow of data originates exclusively from the network client, such as Retrofit or Ktor, wrapped in Kotlin Flows. In an Android environment, associating these data streams with the lifecycle of a Viewmodel Partial Success Refresh Failure or another scoped component is essential to prevent memory leaks and redundant network requests. If a coroutine collector outlives the screen it serves, it can attempt to update destroyed views or hold onto references longer than intended.

To manage this safely, you expose cold flows from your remote data source and transform them within lifecycle-aware boundaries. Using operators like shareIn or stateIn allows you to share upstream flows among multiple collectors while tying the upstream coroutine scope to the application lifecycle or a specific ViewModel scope. When the associated UI component disappears, the active collection stops, cancelling underlying network operations if appropriate and freeing system resources.

Implementing a Remote-Only Repository Pattern

Consider a concrete implementation of a remote-only repository in Kotlin. This example demonstrates how to wrap a remote API call, handle loading and error states using a unified response wrapper, and expose the result as a Flow without touching a local database.

package com.example.repository

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.Response

sealed interface Resource<out T> {
    data class Success<out T>(val data: T) : Resource<T>
    data class Error(val message: String, val throwable: Throwable? = null) : Resource<Nothing>
    object Loading : Resource<Nothing>
}

interface ApiService {
    suspend fun fetchItems(): Response<List<ItemDto>>
}

data class ItemDto(val id: String, val name: String)
data class Item(val id: String, val displayName: String)

class RemoteOnlyItemRepository(private val apiService: ApiService) {

    fun getItems(): Flow<Resource<List<Item>>> = flow {
        emit(Resource.Loading)
        try {
            val response = apiService.fetchItems()
            if (response.isSuccessful && response.body() != null) {
                val remoteData = response.body()!!
                val domainData = remoteData.map { dto ->
                    Item(id = dto.id, displayName = dto.name.trim())
                }
                emit(Resource.Success(domainData))
            } else {
                emit(Resource.Error("Server returned error code: ${response.code()}"))
            }
        } catch (e: Exception) {
            emit(Resource.Error(e.localizedMessage ?: "Unknown network error", e))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this configuration, the repository acts as a clean bridge between the network client and the presentation layer. It maps raw data transfer objects into domain models, wraps errors gracefully, and communicates loading progress without requiring disk caching.

Managing UI States and Error Boundaries

Because a remote-only repository does not provide cached fallback data, your user interface must handle loading, empty, success, and failure states explicitly. When a network request fails due to a dropped connection, the UI cannot display stale records. Instead, it must present a clear error view with an explicit retry action.

Your presentation layer should collect the flow from the repository using lifecycle-aware collection helpers such as repeatOnLifecycle. This ensures that collection pauses when the application goes to the background and resumes or restarts when the user returns, depending on your caching requirements within the ViewModel. By separating observed symptoms, such as a frozen UI during a network timeout, from the root cause, which might be an unhandled exception in the flow collection, you can build predictable diagnostic workflows.

Summary of Architectural Trade-Offs

Choosing between a remote-only repository and a persistent local cache depends entirely on product requirements rather than technical dogma. A remote-only approach reduces codebase complexity, eliminates database maintenance overhead, and ensures users always see fresh data from the server. It requires disciplined state management, robust error handling, and careful lifecycle integration to prevent application crashes and memory leaks.

By leveraging Kotlin Flows and structuring your network boundaries cleanly, you can build maintainable Android features without the weight of unnecessary local storage. Always evaluate whether your feature truly needs offline persistence before introducing the complexity of a local database.

Top comments (0)