2026-02-25T00-51-59_auto_memory/memories.db-wal
This commit is contained in:
parent
abb38b3756
commit
0eccfe5f1f
@ -153,6 +153,15 @@
|
||||
"skillFolderHash": "c5ce954ef1fca62dd177cd1fd16e2fc81898458a",
|
||||
"installedAt": "2026-02-25T00:51:33.360Z",
|
||||
"updatedAt": "2026-02-25T00:51:33.360Z"
|
||||
},
|
||||
"mobile-offline-support": {
|
||||
"source": "aj-geddes/useful-ai-prompts",
|
||||
"sourceType": "github",
|
||||
"sourceUrl": "https://github.com/aj-geddes/useful-ai-prompts.git",
|
||||
"skillPath": "skills/mobile-offline-support/SKILL.md",
|
||||
"skillFolderHash": "05bb4c1a9463fe22342c09422760c5526d2dac06",
|
||||
"installedAt": "2026-02-25T00:51:58.400Z",
|
||||
"updatedAt": "2026-02-25T00:51:58.400Z"
|
||||
}
|
||||
},
|
||||
"dismissed": {
|
||||
|
||||
Binary file not shown.
466
skills/mobile-offline-support/SKILL.md
Normal file
466
skills/mobile-offline-support/SKILL.md
Normal file
@ -0,0 +1,466 @@
|
||||
---
|
||||
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 (
|
||||
<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**
|
||||
|
||||
```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> = 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<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
|
||||
Loading…
x
Reference in New Issue
Block a user