Claude Code Plugins

Community-maintained marketplace

Feedback

mobile-offline-support

@aj-geddes/useful-ai-prompts
4
0

Implement offline-first mobile apps with local storage, sync strategies, and conflict resolution. Covers AsyncStorage, Realm, SQLite, and background sync patterns.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

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 (
    <View style={styles.container}>
      {!isOnline && <Text style={styles.offline}>Offline Mode</Text>}
      {syncing && <ActivityIndicator size="large" />}
      <FlatList
        data={items}
        renderItem={({ item }) => <ItemCard item={item} />}
        keyExtractor={item => item.id}
      />
    </View>
  );
}

2. iOS Core Data Implementation

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> = 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

@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<List<ItemEntity>>

  @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<ActionQueueEntity>

  @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<List<Item>>(emptyList())
  val items: StateFlow<List<Item>> = _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