From everything-claude-code-mobile
Implements offline-first patterns including NetworkBoundResource, Resource wrapper, cache-first/network-first strategies for Kotlin apps with local DB and API sync.
npx claudepluginhub ahmed3elshaer/everything-claude-code-mobile --plugin everything-claude-code-mobileThis skill uses the workspace's default tool permissions.
The core abstraction that coordinates cache and network data sources.
Implements offline-first React Native apps using AsyncStorage for local storage, sync queues with limits, NetInfo for connectivity, and server sync. Use for offline data handling, sync conflicts, queue management, storage limits, network errors.
Implements offline-first mobile apps with local storage (AsyncStorage, Realm, SQLite), sync strategies, background sync, and conflict resolution.
Guides implementing offline-first Capacitor apps with data synchronization, caching, conflict resolution, Fast SQL, service workers, and network detection. For apps needing offline functionality.
Share bugs, ideas, or general feedback.
The core abstraction that coordinates cache and network data sources.
inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline shouldFetch: (ResultType) -> Boolean = { true },
crossinline onFetchFailed: (Throwable) -> Unit = { }
): Flow<Resource<ResultType>> = flow {
emit(Resource.Loading())
val cachedData = query().first()
if (shouldFetch(cachedData)) {
emit(Resource.Loading(cachedData))
try {
val fetchedData = fetch()
saveFetchResult(fetchedData)
} catch (e: Exception) {
onFetchFailed(e)
}
}
emitAll(query().map { Resource.Success(it) })
}
sealed class Resource<out T> {
data class Success<T>(val data: T) : Resource<T>()
data class Loading<T>(val data: T? = null) : Resource<T>()
data class Error<T>(val message: String, val data: T? = null) : Resource<T>()
}
class ArticleRepository(
private val api: ArticleApi,
private val dao: ArticleDao,
private val cachePolicy: CachePolicy
) {
fun getArticles(): Flow<Resource<List<Article>>> = networkBoundResource(
query = { dao.observeAll() },
fetch = { api.getArticles() },
saveFetchResult = { articles ->
dao.transaction {
dao.deleteAll()
dao.insertAll(articles.map { it.toEntity() })
}
},
shouldFetch = { cachedArticles ->
cachedArticles.isEmpty() || cachePolicy.isExpired("articles")
}
)
}
fun getCacheFirst(): Flow<Resource<List<Item>>> = flow {
emit(Resource.Loading())
val cached = dao.getAll().first()
if (cached.isNotEmpty()) {
emit(Resource.Success(cached))
}
try {
val fresh = api.fetchAll()
dao.replaceAll(fresh.map { it.toEntity() })
} catch (e: Exception) {
if (cached.isEmpty()) emit(Resource.Error(e.message ?: "Network error"))
}
emitAll(dao.getAll().map { Resource.Success(it) })
}
fun getNetworkFirst(): Flow<Resource<List<Item>>> = flow {
emit(Resource.Loading())
try {
val fresh = api.fetchAll()
dao.replaceAll(fresh.map { it.toEntity() })
emitAll(dao.getAll().map { Resource.Success(it) })
} catch (e: Exception) {
val cached = dao.getAll().first()
if (cached.isNotEmpty()) {
emit(Resource.Success(cached))
} else {
emit(Resource.Error(e.message ?: "No data available"))
}
}
}
class CachePolicy(private val prefs: SharedPreferences) {
fun isExpired(key: String, ttlMillis: Long = DEFAULT_TTL): Boolean {
val lastFetch = prefs.getLong("cache_ts_$key", 0L)
return System.currentTimeMillis() - lastFetch > ttlMillis
}
fun markFresh(key: String) {
prefs.edit().putLong("cache_ts_$key", System.currentTimeMillis()).apply()
}
fun invalidate(key: String) {
prefs.edit().remove("cache_ts_$key").apply()
}
companion object {
const val DEFAULT_TTL = 15 * 60 * 1000L // 15 minutes
const val SHORT_TTL = 2 * 60 * 1000L // 2 minutes
const val LONG_TTL = 24 * 60 * 60 * 1000L // 24 hours
}
}
class AndroidConnectivityMonitor(context: Context) : ConnectivityMonitor {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override val isConnected: Flow<Boolean> = callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { trySend(true) }
override fun onLost(network: Network) { trySend(false) }
override fun onUnavailable() { trySend(false) }
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
// Emit initial state
trySend(connectivityManager.activeNetwork != null)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}.distinctUntilChanged()
}
import Network
class ConnectivityMonitor: ObservableObject {
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "ConnectivityMonitor")
@Published var isConnected = true
init() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
}
}
monitor.start(queue: queue)
}
deinit { monitor.cancel() }
}
@Entity(tableName = "pending_operations")
data class PendingOperation(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val operationType: String, // "CREATE", "UPDATE", "DELETE"
val entityType: String, // "article", "comment"
val entityId: String,
val payload: String, // JSON-serialized body
val createdAt: Long = System.currentTimeMillis(),
val retryCount: Int = 0
)
class SyncQueue(
private val pendingOpsDao: PendingOperationDao,
private val connectivityMonitor: ConnectivityMonitor
) {
suspend fun enqueue(operation: PendingOperation) {
pendingOpsDao.insert(operation)
if (connectivityMonitor.isCurrentlyConnected()) {
processQueue()
}
}
suspend fun processQueue() {
val pending = pendingOpsDao.getAllPending()
for (op in pending) {
try {
executeSyncOperation(op)
pendingOpsDao.delete(op)
} catch (e: Exception) {
if (op.retryCount >= MAX_RETRIES) {
pendingOpsDao.delete(op)
} else {
pendingOpsDao.update(op.copy(retryCount = op.retryCount + 1))
}
}
}
}
}
suspend fun resolveConflictLastWriteWins(
local: SyncEntity,
remote: SyncEntity
): SyncEntity {
return if (local.updatedAt >= remote.updatedAt) local else remote
}
suspend fun resolveConflictMerge(
base: Article,
local: Article,
remote: Article
): Article {
return Article(
id = base.id,
title = if (local.title != base.title) local.title else remote.title,
body = if (local.body != base.body) local.body else remote.body,
updatedAt = maxOf(local.updatedAt, remote.updatedAt)
)
}
suspend fun <T> retryWithBackoff(
maxRetries: Int = 3,
initialDelay: Long = 1000L,
maxDelay: Long = 30_000L,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(maxRetries - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
}
return block() // final attempt, let exception propagate
}
class SyncWorker(
context: Context,
params: WorkerParameters,
private val syncQueue: SyncQueue
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
syncQueue.processQueue()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
// Schedule periodic sync
fun scheduleSyncWork(workManager: WorkManager) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
workManager.enqueueUniquePeriodicWork(
"sync_work",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
}
import BackgroundTasks
func registerBackgroundSync() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.sync",
using: nil
) { task in
handleSync(task: task as! BGProcessingTask)
}
}
func scheduleSync() {
let request = BGProcessingTaskRequest(identifier: "com.example.sync")
request.requiresNetworkConnectivity = true
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
func handleSync(task: BGProcessingTask) {
let syncTask = Task {
do {
try await SyncService.shared.processQueue()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = { syncTask.cancel() }
}