
Mobile Offline Support
- 312 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
mobile-offline-support is a secondsky/claude-skills module that helps developers design offline-first mobile experiences with local caching, sync queues, and connectivity-aware UI behavior.
About
mobile-offline-support is a skill in secondsky/claude-skills for mobile clients that must work on unreliable networks, though its published description is generic and the readme is empty. From the name and catalog context, it guides agents through offline-first patterns such as local persistence, action queues that sync on reconnect, conflict handling, and user-facing connectivity states. Mobile engineers reach for mobile-offline-support when field apps, travel products, or low-connectivity regions break naive online-only assumptions. Treat outputs as architectural recommendations to validate against your platform stack—React Native, Flutter, or native—because the skill manifest lacks platform-specific detail.
- mobile-offline-support
Mobile Offline Support by the numbers
- 312 all-time installs (skills.sh)
- +14 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,311 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill mobile-offline-supportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 312 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
How do you build offline-first mobile app sync?
Use mobile-offline-support for development tasks
Who is it for?
Mobile developers shipping apps that must remain usable during intermittent connectivity or fully offline sessions.
Skip if: Server-only APIs, desktop web SPAs without mobile offline requirements, or pure UI mockups with no sync design.
When should I use this skill?
The user asks for offline mobile support, sync queues, local caching on devices, or connectivity-aware mobile UX patterns.
What you get
Offline cache strategy, sync queue design, connectivity UI states, and conflict-handling notes for mobile clients.
- Offline sync design
- Cache and queue patterns
Files
Mobile Offline Support
Build offline-first mobile applications with local storage and synchronization.
React Native Implementation
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';
class OfflineManager {
constructor() {
this.syncQueue = [];
this.isOnline = true;
// Maximum items in sync queue before discarding oldest
this.MAX_SYNC_QUEUE_LENGTH = 1000;
NetInfo.addEventListener(state => {
this.isOnline = state.isConnected;
if (this.isOnline) this.processQueue();
});
}
/**
* Fetch data from server.
* TODO: Replace with actual API endpoint implementation.
*/
async fetchFromServer(key) {
try {
// Example implementation - replace with your API
const response = await fetch(`${API_BASE_URL}/data/${key}`);
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('fetchFromServer failed:', error);
throw new Error(`Failed to fetch ${key}: ${error.message}`);
}
}
/**
* Sync data to server.
* TODO: Replace with actual API endpoint implementation.
*/
async syncToServer(key, data) {
try {
// Example implementation - replace with your API
const response = await fetch(`${API_BASE_URL}/data/${key}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('syncToServer failed:', error);
throw new Error(`Failed to sync ${key}: ${error.message}`);
}
}
async getData(key) {
const cached = await AsyncStorage.getItem(key);
if (cached) return JSON.parse(cached);
if (this.isOnline) {
const data = await this.fetchFromServer(key);
await AsyncStorage.setItem(key, JSON.stringify(data));
return data;
}
return null;
}
async saveData(key, data) {
await AsyncStorage.setItem(key, JSON.stringify(data));
if (this.isOnline) {
await this.syncToServer(key, data);
} else {
// Add to queue
this.syncQueue.push({ key, data, timestamp: Date.now() });
// Enforce queue bounds - discard oldest if exceeded
while (this.syncQueue.length > this.MAX_SYNC_QUEUE_LENGTH) {
const discarded = this.syncQueue.shift();
console.warn(`Sync queue full - discarded oldest item: ${discarded.key}`);
}
// Persist trimmed queue
await AsyncStorage.setItem('syncQueue', JSON.stringify(this.syncQueue));
}
}
async processQueue() {
const failedItems = [];
for (const item of this.syncQueue) {
try {
await this.syncToServer(item.key, item.data);
} catch (err) {
console.error('Sync failed:', err);
failedItems.push(item);
}
}
this.syncQueue = failedItems;
if (failedItems.length === 0) {
await AsyncStorage.removeItem('syncQueue');
} else {
await AsyncStorage.setItem('syncQueue', JSON.stringify(failedItems));
}
}
}Conflict Resolution
function resolveConflict(local, server) {
// Last-write-wins
if (local.updatedAt > server.updatedAt) return local;
return server;
// Or merge changes
// return { ...server, ...local };
}UI Indicators
function OfflineIndicator() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
return NetInfo.addEventListener(state => {
setIsOnline(state.isConnected);
});
}, []);
if (isOnline) return null;
return (
<View style={styles.banner}>
<Text>You're offline. Changes will sync when connected.</Text>
</View>
);
}Best Practices
- Cache frequently accessed data locally
- Queue actions for later sync
- Show clear offline indicators
- Handle sync conflicts gracefully
- Compress stored data
- Test offline scenarios thoroughly
Native Implementations
See references/native-implementations.md for:
- iOS Core Data with sync manager
- Android Room database with WorkManager sync
Avoid
- Assuming connectivity
- Losing data on sync failures
- Unbounded queue growth
- Syncing sensitive data insecurely
iOS Core Data Offline Support
Complete iOS implementation with Core Data and network monitoring.
import Foundation
import CoreData
import Network
// MARK: - Persistence Controller
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "OfflineModel")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
var viewContext: NSManagedObjectContext {
container.viewContext
}
func saveContext() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
print("Error saving context: \(error)")
}
}
}
}
// MARK: - Core Data Entities
@objc(CachedItem)
public class CachedItem: NSManagedObject {
@NSManaged public var id: String
@NSManaged public var data: Data
@NSManaged public var updatedAt: Date
@NSManaged public var syncStatus: String // "synced", "pending", "failed"
}
@objc(PendingAction)
public class PendingAction: NSManagedObject {
@NSManaged public var id: String
@NSManaged public var actionType: String
@NSManaged public var payload: Data
@NSManaged public var createdAt: Date
@NSManaged public var retryCount: Int16
}
// MARK: - Network Monitor
class NetworkMonitor: ObservableObject {
static let shared = NetworkMonitor()
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
@Published var isConnected = true
@Published var connectionType: ConnectionType = .unknown
enum ConnectionType {
case wifi, cellular, ethernet, unknown
}
private init() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
if path.usesInterfaceType(.wifi) {
self?.connectionType = .wifi
} else if path.usesInterfaceType(.cellular) {
self?.connectionType = .cellular
} else if path.usesInterfaceType(.wiredEthernet) {
self?.connectionType = .ethernet
} else {
self?.connectionType = .unknown
}
if path.status == .satisfied {
self?.processPendingActions()
}
}
}
monitor.start(queue: queue)
}
private func processPendingActions() {
OfflineSyncManager.shared.syncPendingActions()
}
}
// MARK: - Offline Sync Manager
class OfflineSyncManager {
static let shared = OfflineSyncManager()
private let context = PersistenceController.shared.viewContext
private let maxRetries = 3
// Cache data locally
func cacheItem<T: Codable>(id: String, item: T) {
let fetchRequest: NSFetchRequest<CachedItem> = CachedItem.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %@", id)
do {
let results = try context.fetch(fetchRequest)
let cachedItem = results.first ?? CachedItem(context: context)
cachedItem.id = id
cachedItem.data = try JSONEncoder().encode(item)
cachedItem.updatedAt = Date()
cachedItem.syncStatus = "synced"
PersistenceController.shared.saveContext()
} catch {
print("Cache error: \(error)")
}
}
// Get cached data
func getCachedItem<T: Codable>(id: String, type: T.Type) -> T? {
let fetchRequest: NSFetchRequest<CachedItem> = CachedItem.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %@", id)
do {
if let cached = try context.fetch(fetchRequest).first {
return try JSONDecoder().decode(type, from: cached.data)
}
} catch {
print("Fetch error: \(error)")
}
return nil
}
// Queue action for later sync
func queueAction(type: String, payload: [String: Any]) {
let action = PendingAction(context: context)
action.id = UUID().uuidString
action.actionType = type
action.payload = try! JSONSerialization.data(withJSONObject: payload)
action.createdAt = Date()
action.retryCount = 0
PersistenceController.shared.saveContext()
}
// Process pending actions when online
func syncPendingActions() {
let fetchRequest: NSFetchRequest<PendingAction> = PendingAction.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)]
do {
let actions = try context.fetch(fetchRequest)
for action in actions {
guard action.retryCount < maxRetries else {
context.delete(action)
continue
}
syncAction(action) { success in
if success {
self.context.delete(action)
} else {
action.retryCount += 1
}
PersistenceController.shared.saveContext()
}
}
} catch {
print("Sync error: \(error)")
}
}
private func syncAction(_ action: PendingAction, completion: @escaping (Bool) -> Void) {
// Implement API call based on action.actionType
// Call completion(true) on success, completion(false) on failure
}
}Android Room Offline Support
// Entity
@Entity(tableName = "cached_items")
data class CachedItem(
@PrimaryKey val id: String,
val data: String,
val updatedAt: Long = System.currentTimeMillis(),
val syncStatus: String = "synced"
)
@Entity(tableName = "pending_actions")
data class PendingAction(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val actionType: String,
val payload: String,
val createdAt: Long = System.currentTimeMillis(),
val retryCount: Int = 0
)
// DAO
@Dao
interface OfflineDao {
@Query("SELECT * FROM cached_items WHERE id = :id")
suspend fun getCachedItem(id: String): CachedItem?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun cacheItem(item: CachedItem)
@Query("SELECT * FROM pending_actions ORDER BY createdAt ASC")
fun getPendingActions(): Flow<List<PendingAction>>
@Insert
suspend fun queueAction(action: PendingAction)
@Delete
suspend fun deleteAction(action: PendingAction)
@Update
suspend fun updateAction(action: PendingAction)
}
// Repository
class OfflineRepository @Inject constructor(
private val dao: OfflineDao,
private val api: ApiService
) {
suspend fun <T> fetchWithCache(
id: String,
fetch: suspend () -> T,
serialize: (T) -> String,
deserialize: (String) -> T
): T {
// Try network first
return try {
val data = fetch()
dao.cacheItem(CachedItem(id, serialize(data)))
data
} catch (e: Exception) {
// Fall back to cache
dao.getCachedItem(id)?.let { deserialize(it.data) }
?: throw e
}
}
fun syncPendingActions() = dao.getPendingActions()
.onEach { actions ->
actions.forEach { action ->
if (action.retryCount < 3) {
try {
// Execute action via API
dao.deleteAction(action)
} catch (e: Exception) {
dao.updateAction(action.copy(retryCount = action.retryCount + 1))
}
}
}
}
}Related skills
FAQ
What does mobile-offline-support cover?
mobile-offline-support guides offline-first mobile design—local caching, sync queues, reconnect behavior, and connectivity UI—for apps that must work without constant network access.
Is mobile-offline-support platform-specific?
mobile-offline-support describes cross-cutting offline patterns; developers still map recommendations to React Native, Flutter, Swift, or Kotlin implementations in their codebase.