mobile-offline-support
About
This skill helps developers implement offline-first mobile applications using local storage and background synchronization. It covers practical implementations with AsyncStorage, Realm, and SQLite while addressing sync strategies and conflict resolution. Use it when building apps that need to work reliably without internet connectivity while maintaining data consistency.
Documentation
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
Quick Install
/plugin add https://github.com/aj-geddes/useful-ai-prompts/tree/main/mobile-offline-supportCopy and paste this command in Claude Code to install this skill
GitHub 仓库
Related Skills
subagent-driven-development
DevelopmentThis skill executes implementation plans by dispatching a fresh subagent for each independent task, with code review between tasks. It enables fast iteration while maintaining quality gates through this review process. Use it when working on mostly independent tasks within the same session to ensure continuous progress with built-in quality checks.
algorithmic-art
MetaThis Claude Skill creates original algorithmic art using p5.js with seeded randomness and interactive parameters. It generates .md files for algorithmic philosophies, plus .html and .js files for interactive generative art implementations. Use it when developers need to create flow fields, particle systems, or other computational art while avoiding copyright issues.
executing-plans
DesignUse the executing-plans skill when you have a complete implementation plan to execute in controlled batches with review checkpoints. It loads and critically reviews the plan, then executes tasks in small batches (default 3 tasks) while reporting progress between each batch for architect review. This ensures systematic implementation with built-in quality control checkpoints.
cost-optimization
OtherThis Claude Skill helps developers optimize cloud costs through resource rightsizing, tagging strategies, and spending analysis. It provides a framework for reducing cloud expenses and implementing cost governance across AWS, Azure, and GCP. Use it when you need to analyze infrastructure costs, right-size resources, or meet budget constraints.
