# Offline First

> |

- **Type:** Skill
- **Install:** `agentstack add skill-piyushverma0-android-agent-skills-offline-first`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [piyushverma0](https://agentstack.voostack.com/s/piyushverma0)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [piyushverma0](https://github.com/piyushverma0)
- **Source:** https://github.com/piyushverma0/android-agent-skills/tree/main/skills/offline-first
- **Website:** https://android-agent-skills.vercel.app

## Install

```sh
agentstack add skill-piyushverma0-android-agent-skills-offline-first
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Offline-First Architecture

Apps that require internet to work lose 30%+ of users. These patterns make your app work
everywhere — with or without connectivity.

## Core principle: Local DB is source of truth

```
User Action → ViewModel → Repository
                               ↓
                         Local DB (Room)  ← single source of truth
                               ↓
                          UI observes Flow from Room
                               ↓ (background)
                         Network sync → update Local DB → UI auto-updates
```

## Rule 1: Single-direction data flow — never mix local + remote in UI

```kotlin
// ✅ Repository: local DB is source of truth, network updates DB silently
class ItemRepositoryImpl @Inject constructor(
    private val itemDao: ItemDao,
    private val api: ItemApiService,
    @IoDispatcher private val dispatcher: CoroutineDispatcher
) : ItemRepository {

    // UI always observes local DB
    override fun getItemsStream(): Flow> =
        itemDao.getAll().map { it.map { entity -> entity.toDomain() } }

    // Sync pulls from network and updates local DB
    override suspend fun sync(): Result = withContext(dispatcher) {
        runCatching {
            val remoteItems = api.getItems()
            itemDao.upsertAll(remoteItems.map { it.toEntity() })
        }
    }
}

// ✅ ViewModel triggers sync but observes local DB
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val repository: ItemRepository
) : ViewModel() {

    val items: StateFlow> = repository.getItemsStream()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

    init { sync() }

    fun sync() {
        viewModelScope.launch {
            repository.sync()   // updates DB → Flow auto-updates UI
        }
    }
}
```

## Rule 2: Optimistic UI updates

```kotlin
// ✅ Apply change locally immediately, sync to server in background
override suspend fun toggleFavorite(itemId: String): Result = withContext(dispatcher) {
    // 1. Update local DB immediately (UI updates instantly)
    val item = itemDao.getByIdOnce(itemId) ?: return@withContext Result.failure(NotFoundException())
    val newValue = !item.isFavorite
    itemDao.updateFavorite(itemId, newValue)

    // 2. Sync to server in background
    runCatching {
        api.patchItem(itemId, mapOf("is_favorite" to newValue))
    }.onFailure {
        // 3. Rollback local change if server fails
        itemDao.updateFavorite(itemId, !newValue)
    }
}
```

## Rule 3: Network connectivity monitoring

```kotlin
// ✅ Observable connectivity status
class NetworkMonitor @Inject constructor(
    @ApplicationContext private val context: Context
) {
    val isOnline: Flow = callbackFlow {
        val connectivityManager = context.getSystemService()!!

        val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) { trySend(true) }
            override fun onLost(network: Network) { trySend(false) }
        }

        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()

        connectivityManager.registerNetworkCallback(request, callback)
        // Emit current state immediately
        trySend(connectivityManager.isCurrentlyConnected())

        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
    }.distinctUntilChanged()
        .shareIn(CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(), 1)

    private fun ConnectivityManager.isCurrentlyConnected(): Boolean =
        activeNetwork?.let { getNetworkCapabilities(it) }
            ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
}

// ✅ In ViewModel — sync when connection is restored
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val repository: ItemRepository,
    private val networkMonitor: NetworkMonitor
) : ViewModel() {
    init {
        viewModelScope.launch {
            networkMonitor.isOnline
                .filter { it }       // only emit when online
                .collect { repository.sync() }
        }
    }
}
```

## Rule 4: WorkManager for background sync

```kotlin
// ✅ Periodic sync with WorkManager
@HiltWorker
class SyncWorker @AssistedInject constructor(
    @Assisted appContext: Context,
    @Assisted workerParams: WorkerParameters,
    private val repository: ItemRepository
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        return try {
            repository.sync().fold(
                onSuccess = { Result.success() },
                onFailure = {
                    if (runAttemptCount (
                repeatInterval = 15,
                repeatIntervalTimeUnit = TimeUnit.MINUTES
            )
                .setConstraints(constraints)
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
                .build()

            workManager.enqueueUniquePeriodicWork(
                WORK_NAME,
                ExistingPeriodicWorkPolicy.KEEP,
                request
            )
        }
    }
}
```

## Rule 5: Pending operations queue — for offline writes

```kotlin
// ✅ Store operations when offline, replay when online
@Entity(tableName = "pending_operations")
data class PendingOperationEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "operation_type") val operationType: String,   // "CREATE", "UPDATE", "DELETE"
    @ColumnInfo(name = "entity_id") val entityId: String,
    @ColumnInfo(name = "payload") val payload: String,                // JSON-serialized request
    @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
    @ColumnInfo(name = "retry_count") val retryCount: Int = 0
)

// ✅ Repository queues when offline
override suspend fun createItem(item: Item): Result = withContext(dispatcher) {
    // Always save locally first
    itemDao.upsert(item.toEntity())

    // Try to sync immediately
    if (networkMonitor.isCurrentlyOnline()) {
        runCatching { api.createItem(item.toRequest()) }
            .onFailure {
                // Queue for later retry
                pendingOpsDao.insert(PendingOperationEntity(
                    operationType = "CREATE",
                    entityId = item.id,
                    payload = Json.encodeToString(item.toRequest())
                ))
            }
    } else {
        // Offline — queue immediately
        pendingOpsDao.insert(PendingOperationEntity(
            operationType = "CREATE",
            entityId = item.id,
            payload = Json.encodeToString(item.toRequest())
        ))
    }

    Result.success(Unit)
}
```

## Rule 6: Stale data indicator

```kotlin
// ✅ Show "last synced" to inform users of data freshness
data class SyncMetadata(
    val lastSyncTime: Instant?,
    val isSyncing: Boolean,
    val syncError: String?
)

@HiltViewModel
class HomeViewModel : ViewModel() {
    private val _syncMetadata = MutableStateFlow(SyncMetadata(null, false, null))
    val syncMetadata: StateFlow = _syncMetadata.asStateFlow()

    private fun sync() {
        viewModelScope.launch {
            _syncMetadata.update { it.copy(isSyncing = true, syncError = null) }
            repository.sync()
                .onSuccess {
                    _syncMetadata.update { it.copy(
                        isSyncing = false,
                        lastSyncTime = Clock.System.now()
                    ) }
                }
                .onFailure { error ->
                    _syncMetadata.update { it.copy(
                        isSyncing = false,
                        syncError = "Last sync failed. Showing cached data."
                    ) }
                }
        }
    }
}
```

## Common Mistakes

❌ Showing loading while Room DB loads — DB is fast, show data immediately
❌ UI observing network response directly — UI must only observe local DB
❌ No retry logic for failed sync — use exponential backoff
❌ Running WorkManager sync on unconstrained network — always require CONNECTED
❌ Not handling sync conflicts — decide on server-wins or last-write-wins policy
❌ Clearing local cache on every fresh load — breaks offline experience

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [piyushverma0](https://github.com/piyushverma0)
- **Source:** [piyushverma0/android-agent-skills](https://github.com/piyushverma0/android-agent-skills)
- **License:** MIT
- **Homepage:** https://android-agent-skills.vercel.app

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-piyushverma0-android-agent-skills-offline-first
- Seller: https://agentstack.voostack.com/s/piyushverma0
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
