--- name: mobile-offline-support description: Implement offline-first mobile apps with local storage, sync strategies, and conflict resolution. Covers AsyncStorage, Realm, SQLite, and background sync patterns. --- # Mobile Offline Support ## Overview Design offline-first mobile applications that provide seamless user experience regardless of connectivity. ## When to Use - Building apps that work without internet connection - Implementing seamless sync when connectivity returns - Handling data conflicts between device and server - Reducing server load with intelligent caching - Improving app responsiveness with local storage ## Instructions ### 1. **React Native Offline Storage** ```javascript import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo from '@react-native-community/netinfo'; class StorageManager { static async saveItems(items) { try { await AsyncStorage.setItem( 'items_cache', JSON.stringify({ data: items, timestamp: Date.now() }) ); } catch (error) { console.error('Failed to save items:', error); } } static async getItems() { try { const data = await AsyncStorage.getItem('items_cache'); return data ? JSON.parse(data) : null; } catch (error) { console.error('Failed to retrieve items:', error); return null; } } static async queueAction(action) { try { const queue = await AsyncStorage.getItem('action_queue'); const actions = queue ? JSON.parse(queue) : []; actions.push({ ...action, id: Date.now(), attempts: 0 }); await AsyncStorage.setItem('action_queue', JSON.stringify(actions)); } catch (error) { console.error('Failed to queue action:', error); } } static async getActionQueue() { try { const queue = await AsyncStorage.getItem('action_queue'); return queue ? JSON.parse(queue) : []; } catch (error) { return []; } } static async removeFromQueue(actionId) { try { const queue = await AsyncStorage.getItem('action_queue'); const actions = queue ? JSON.parse(queue) : []; const filtered = actions.filter(a => a.id !== actionId); await AsyncStorage.setItem('action_queue', JSON.stringify(filtered)); } catch (error) { console.error('Failed to remove from queue:', error); } } } class OfflineAPIService { async fetchItems() { const isOnline = await this.checkConnectivity(); if (isOnline) { try { const response = await fetch('https://api.example.com/items'); const items = await response.json(); await StorageManager.saveItems(items); return items; } catch (error) { const cached = await StorageManager.getItems(); return cached?.data || []; } } else { const cached = await StorageManager.getItems(); return cached?.data || []; } } async createItem(item) { const isOnline = await this.checkConnectivity(); if (isOnline) { try { const response = await fetch('https://api.example.com/items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item) }); const created = await response.json(); return { success: true, data: created }; } catch (error) { await StorageManager.queueAction({ type: 'CREATE_ITEM', payload: item }); return { success: false, queued: true }; } } else { await StorageManager.queueAction({ type: 'CREATE_ITEM', payload: item }); return { success: false, queued: true }; } } async syncQueue() { const queue = await StorageManager.getActionQueue(); for (const action of queue) { try { await this.executeAction(action); await StorageManager.removeFromQueue(action.id); } catch (error) { action.attempts = (action.attempts || 0) + 1; if (action.attempts > 3) { await StorageManager.removeFromQueue(action.id); } } } } private async executeAction(action) { switch (action.type) { case 'CREATE_ITEM': return fetch('https://api.example.com/items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(action.payload) }); default: return Promise.reject(new Error('Unknown action type')); } } async checkConnectivity() { const state = await NetInfo.fetch(); return state.isConnected ?? false; } } export function OfflineListScreen() { const [items, setItems] = useState([]); const [isOnline, setIsOnline] = useState(true); const [syncing, setSyncing] = useState(false); const apiService = new OfflineAPIService(); useFocusEffect( useCallback(() => { loadItems(); const unsubscribe = NetInfo.addEventListener(state => { setIsOnline(state.isConnected ?? false); if (state.isConnected) { syncQueue(); } }); return unsubscribe; }, []) ); const loadItems = async () => { const items = await apiService.fetchItems(); setItems(items); }; const syncQueue = async () => { setSyncing(true); await apiService.syncQueue(); await loadItems(); setSyncing(false); }; return ( {!isOnline && Offline Mode} {syncing && } } keyExtractor={item => item.id} /> ); } ``` ### 2. **iOS Core Data Implementation** ```swift import CoreData class PersistenceController { static let shared = PersistenceController() let container: NSPersistentContainer init(inMemory: Bool = false) { container = NSPersistentContainer(name: "MyApp") if inMemory { container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null") } container.loadPersistentStores { _, error in if let error = error as NSError? { print("Core Data load error: \(error)") } } container.viewContext.automaticallyMergesChangesFromParent = true } func save(_ context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) { if context.hasChanges { do { try context.save() } catch { print("Save error: \(error)") } } } } // Core Data Models @NSManaged class ItemEntity: NSManagedObject { @NSManaged var id: String @NSManaged var title: String @NSManaged var description: String? @NSManaged var isSynced: Bool } @NSManaged class ActionQueueEntity: NSManagedObject { @NSManaged var id: UUID @NSManaged var type: String @NSManaged var payload: Data? @NSManaged var createdAt: Date } class OfflineSyncManager: NSObject, ObservableObject { @Published var isOnline = true @Published var isSyncing = false private let networkMonitor = NWPathMonitor() private let persistenceController = PersistenceController.shared override init() { super.init() setupNetworkMonitoring() } private func setupNetworkMonitoring() { networkMonitor.pathUpdateHandler = { [weak self] path in DispatchQueue.main.async { self?.isOnline = path.status == .satisfied if path.status == .satisfied { self?.syncWithServer() } } } let queue = DispatchQueue(label: "NetworkMonitor") networkMonitor.start(queue: queue) } func saveItem(_ item: Item) { let context = persistenceController.container.viewContext let entity = ItemEntity(context: context) entity.id = item.id entity.title = item.title entity.isSynced = false persistenceController.save(context) if isOnline { syncItem(item) } } func syncWithServer() { isSyncing = true let context = persistenceController.container.viewContext let request: NSFetchRequest = ActionQueueEntity.fetchRequest() do { let pendingActions = try context.fetch(request) for action in pendingActions { context.delete(action) } persistenceController.save(context) } catch { print("Sync error: \(error)") } isSyncing = false } } ``` ### 3. **Android Room Database** ```kotlin @Entity(tableName = "items") data class ItemEntity( @PrimaryKey val id: String, val title: String, val description: String?, val isSynced: Boolean = false ) @Entity(tableName = "action_queue") data class ActionQueueEntity( @PrimaryKey val id: Long = System.currentTimeMillis(), val type: String, val payload: String, val createdAt: Long = System.currentTimeMillis() ) @Dao interface ItemDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertItem(item: ItemEntity) @Query("SELECT * FROM items") fun getAllItems(): Flow> @Update suspend fun updateItem(item: ItemEntity) } @Dao interface ActionQueueDao { @Insert suspend fun insertAction(action: ActionQueueEntity) @Query("SELECT * FROM action_queue ORDER BY createdAt ASC") suspend fun getAllActions(): List @Delete suspend fun deleteAction(action: ActionQueueEntity) } @Database(entities = [ItemEntity::class, ActionQueueEntity::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun itemDao(): ItemDao abstract fun actionQueueDao(): ActionQueueDao } @HiltViewModel class OfflineItemsViewModel @Inject constructor( private val itemDao: ItemDao, private val actionQueueDao: ActionQueueDao, private val connectivityManager: ConnectivityManager ) : ViewModel() { private val _items = MutableStateFlow>(emptyList()) val items: StateFlow> = _items.asStateFlow() init { viewModelScope.launch { itemDao.getAllItems().collect { entities -> _items.value = entities.map { it.toItem() } } } observeNetworkConnectivity() } fun saveItem(item: Item) { viewModelScope.launch { val entity = item.toEntity() itemDao.insertItem(entity) if (isNetworkAvailable()) { syncItem(item) } else { actionQueueDao.insertAction( ActionQueueEntity( type = "CREATE_ITEM", payload = Json.encodeToString(item) ) ) } } } private fun observeNetworkConnectivity() { val networkRequest = NetworkRequest.Builder() .addCapability(NET_CAPABILITY_INTERNET) .build() connectivityManager.registerNetworkCallback( networkRequest, object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { viewModelScope.launch { syncQueue() } } } ) } private suspend fun syncQueue() { val queue = actionQueueDao.getAllActions() for (action in queue) { try { actionQueueDao.deleteAction(action) } catch (e: Exception) { println("Sync error: ${e.message}") } } } private fun isNetworkAvailable(): Boolean { val activeNetwork = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false return capabilities.hasCapability(NET_CAPABILITY_INTERNET) } } ``` ## Best Practices ### ✅ DO - Implement robust local storage - Use automatic sync when online - Provide visual feedback for offline status - Queue actions for later sync - Handle conflicts gracefully - Cache frequently accessed data - Implement proper error recovery - Test offline scenarios thoroughly - Use compression for large data - Monitor storage usage ### ❌ DON'T - Assume constant connectivity - Sync large files frequently - Ignore storage limitations - Force unnecessary syncing - Lose data on offline mode - Store sensitive data unencrypted - Accumulate infinite queue items - Ignore sync failures silently - Sync in tight loops - Deploy without offline testing