[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6D1ZNSA90DD4M0ACHTYKGSZ":3,"skill-same:01M6D1ZNSA90DD4M0ACHTYKGSZ":103},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":49,"linkOnly":50,"markdownContent":51,"markdownBody":52,"frontMatter":53,"refName":54,"commitSha":55,"skillDirectory":56,"forksCount":57,"repositoryHtmlUrl":58,"repositoryHomepageUrl":59,"ownerHtmlUrl":60,"ownerBlogUrl":59,"ownerDescription":61,"updatedAt":62,"install":63,"resources":76},"01M6D1ZNSA90DD4M0ACHTYKGSZ","mobile-data",null,"Data on a phone that is often offline: a local database as the source of truth, syncing with the server (pull by cursor, push through a queue with idempotency keys), conflicts, retries and timeouts, caching, secure storage for tokens, local schema migrations, background sync limits, and large downloads. Read before building features that load, store or sync data in a mobile app.","enowdev\u002Fenowxcli","enowdev","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F179420349?v=4","01M6D1MVX2KC0X3Z9NESE7P22K","Enow Dev.","01M6D1MW1FMZGBDKWAJY4GQX81",67,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fmobile-data\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48],"mobile data","mobile","data","before building","tokens","local","caching","phone","conflicts","mobile app","read","storage","downloads","server","offline","database","push","pull","secure","sync","2026-11-10 10:12:30",false,"---\nname: mobile-data\ndescription: \"Data on a phone that is often offline: a local database as the source of truth, syncing with the server (pull by cursor, push through a queue with idempotency keys), conflicts, retries and timeouts, caching, secure storage for tokens, local schema migrations, background sync limits, and large downloads. Read before building features that load, store or sync data in a mobile app.\"\n---\n\n# Data on a phone that is often offline\n\nThe naive mobile data layer: every screen fetches when it opens and shows a\nspinner, nothing works on a train, a failed save is simply lost, a retry\ncreates a second order, two devices overwrite each other in silence, the\ntoken sits in plain preferences, an update's migration wipes the local\ndata, and a 2 GB download restarts from zero after every dropped\nconnection. This skill is the design that survives real networks. Stack\ndetails are in `mobile-ios`, `mobile-android`, `mobile-react-native` and\n`mobile-flutter`; the server side of the contract in `backend-api`.\n\n## 1. Decide how offline the app is\n\n| Level | Works offline | Build |\n|---|---|---|\n| Online with a cache | Recent reads; writes need a connection | HTTP cache or a persisted query cache; clear write errors |\n| Offline reads | Everything synced can be browsed; writes fail fast with Retry | A local database filled by sync |\n| Offline-first | Read and write everything; sync when possible | Local database as the truth, an outbox, a conflict policy, a server change feed |\n\n- Offline-first for apps used on the move: field work, inspections,\n  delivery, notes, travel, logs.\n- Some commits need the server at that moment (a payment, a booking against\n  stock): keep the user's intent locally and say plainly that it is not\n  done until confirmed.\n\n## 2. The local database is what the UI reads\n\n- The UI observes the local database (Room `Flow`, SwiftData `@Query`,\n  Drift `watch()`, WatermelonDB observables, Drizzle live queries on\n  expo-sqlite). Network responses are written into it; screens update from\n  it, never straight from a response.\n\n| Stack | Local store | Sync engines to consider |\n|---|---|---|\n| Android | Room | PowerSync, Firestore, Couchbase Lite |\n| iOS | SwiftData, Core Data, GRDB | CloudKit (private data), PowerSync |\n| Flutter | Drift | PowerSync, Firestore |\n| React Native | expo-sqlite with Drizzle, op-sqlite, WatermelonDB | PowerSync, WatermelonDB's sync protocol |\n\n- Realm's Atlas Device Sync reached end of life in September 2025: do not\n  start on it; plan the move where it is still in use.\n- Ids generated on the device (UUID v7, time-ordered, or v4), so records\n  created offline have stable ids before the server sees them.\n- Ordering comes from the server (a version or its timestamp), never from\n  the device clock, which users and time zones change.\n\n## 3. Pull: changes since a cursor\n\n```\nGET \u002Fsync\u002Forders?cursor=eyJ2IjoxMjM0fQ&limit=500\n200 { \"changes\": [{ \"id\": \"0192…\", \"version\": 1235, \"deletedAt\": null, \"status\": \"paid\" }],\n      \"cursor\": \"eyJ2IjoxMjM1fQ\", \"hasMore\": false }\n```\n\n- The server stamps each change with an increasing version (a sequence set\n  in the same transaction as the write); the cursor is opaque.\n- Deletes are tombstones (`deletedAt`), kept for a set window (30 to 90\n  days). A client whose cursor is older than the window does a full resync.\n- Apply each page and its new cursor in one local transaction, so a crash\n  never keeps one without the other. Page until `hasMore` is false.\n- Pull on launch and foreground, after a push, when a push notification\n  says something changed, and in background work when the OS allows.\n- Sync only what the user needs (their records, recent months).\n\n## 4. Push: an outbox with idempotency keys\n\n- Each local write is one transaction: the row changes (marked pending) and\n  an outbox row is added: entity, operation, payload, idempotency key,\n  attempts, next attempt time, last error.\n- One sender drains the outbox in order. The idempotency key is created\n  once, when the user acted, and sent on every retry\n  (`Idempotency-Key`); the server returns the stored result for a repeat.\n- Success applies the server's version to the row and removes the outbox\n  entry. Transient failures (offline, timeout, 5xx, 429) back off. Permanent\n  ones (validation, 403, an unresolved 409) mark the item failed and show\n  it to the user with fix or discard, never retried forever.\n- Parents before children: a record created offline that references\n  another created offline waits for it.\n\n```ts\nexport async function drainOutbox(db: Db, api: Api, signal: AbortSignal) {\n  for (const item of await db.outbox.due(Date.now())) {\n    if (signal.aborted) return;\n    try {\n      const saved = await api.send(item.op, item.payload, { idempotencyKey: item.key, signal });\n      await db.transaction(async (tx) => {\n        await tx.records.applyServerVersion(saved);\n        await tx.outbox.remove(item.id);\n      });\n    } catch (e) {\n      if (isPermanent(e)) await db.outbox.markFailed(item.id, errorCode(e));\n      else await db.outbox.reschedule(item.id, Date.now() + backoffMs(item.attempts));\n      if (isOffline(e)) return; \u002F\u002F wait for connectivity instead of failing every item\n    }\n  }\n}\n```\n\n## 5. Conflicts\n\n| Policy | Use for | Cost |\n|---|---|---|\n| Server authority: stale versions get `409` | Money, stock, bookings, anything with rules | The user reloads and redoes the change |\n| Last write wins, in the server's order | One person's data across their devices, low-stakes fields | Concurrent edits are lost silently |\n| Field-level merge against a base version | Profiles, forms, records several people edit | More server logic |\n| CRDTs (Yjs, Automerge) | Collaborative text and lists edited offline | Complexity, growing history |\n\n- Every update carries the base `version` it was made from; the server\n  compares, applies the policy, and returns the current record on conflict.\n- Tell the user when something could not sync: a visible state on the item\n  (\"Not synced\", with Retry and Discard), never a silent drop.\n\n## 6. Network reality\n\n- Timeouts on every request: about 10 seconds to connect, 15 to 30 seconds\n  in total for an API call, longer for uploads by size. Defaults are wrong:\n  URLSession waits 60 seconds, `fetch` and Dart's `http` never time out.\n- Retry only idempotent requests (GET, PUT, DELETE, and POST with an\n  idempotency key), with exponential backoff and full jitter, capped:\n\n```ts\nconst backoffMs = (attempt: number) => Math.random() * Math.min(60_000, 1_000 * 2 ** attempt);\n```\n\n- Honour `Retry-After` on `429` and `503`; stop after about 5 attempts in\n  the foreground and leave the rest to the outbox.\n- Reachability (`NWPathMonitor`, `ConnectivityManager`, NetInfo,\n  connectivity_plus) is a hint: a connected network may be a captive\n  portal. Always try and handle failure; use it to resume sync and show\n  the offline banner.\n- Fewer round trips: one sync call with many changes, pages of 50 to 500\n  rows, only the fields needed, compressed responses (the platform clients\n  decode gzip for you). Cancel requests when their screen leaves.\n- On metered or Low Data connections (`isActiveNetworkMetered`,\n  `NWPath.isConstrained`), defer large optional downloads.\n\n## 7. Caching\n\n- Images through the platform or library cache (Coil, Nuke, Kingfisher,\n  expo-image, cached_network_image), from URLs that change when the content\n  does, so caches never go stale.\n- HTTP caching: `Cache-Control` and `ETag` with `If-None-Match` turn repeat\n  loads into `304`s; OkHttp needs a `Cache` configured to use them.\n- TanStack Query in React Native: persist the cache with a `maxAge` (24\n  hours) and a `gcTime` at least as long, and resume paused mutations after\n  the restore (`onSuccess={() => queryClient.resumePausedMutations()}` on\n  `PersistQueryClientProvider`).\n- Show cached data at once with its age (\"Updated 5 min ago\") and refresh\n  behind it.\n\n## 8. Secure storage\n\n- Tokens in the Keychain or Keystore-backed storage (expo-secure-store,\n  flutter_secure_storage, `mobile-ios`, `mobile-android`); never\n  AsyncStorage, UserDefaults, SharedPreferences, plain SQLite, logs or\n  crash reports.\n- The access token short-lived and in memory; the refresh token in secure\n  storage; one refresh in flight at a time.\n- Sign-out clears tokens, the user's local data, caches and the outbox\n  (warn first when changes are unsynced: \"3 changes are not synced yet\"),\n  and unregisters the push token.\n- Sensitive databases encrypted with SQLCipher (op-sqlite, GRDB, Room with a\n  SQLCipher factory), the key in the Keychain or Keystore; iOS files with\n  complete data protection.\n\n## 9. Local schema migrations\n\n- The schema has a version; migrations run in order from any shipped\n  version to the current one. Test upgrading a real database file from\n  every version still installed (Room `MigrationTestHelper`, Drift schema\n  tests, SwiftData migration plans).\n- Adding is cheap; renames and type changes copy through a new table. A\n  destructive fallback only for caches that can be downloaded again.\n\n```ts\nconst row = await db.getFirstAsync\u003C{ user_version: number }>(\"PRAGMA user_version\");\nconst current = row?.user_version ?? 0;\nfor (const [version, sql] of MIGRATIONS) { \u002F\u002F [[1, \"CREATE TABLE ...\"], [2, \"ALTER TABLE ...\"]]\n  if (version \u003C= current) continue;\n  await db.withTransactionAsync(async () => {\n    await db.execAsync(sql);\n    await db.execAsync(`PRAGMA user_version = ${version}`);\n  });\n}\n```\n\n- Old app versions stay installed for months: the sync API accepts them, or\n  the app checks a minimum supported version at launch and shows \"Update\n  required\".\n\n## 10. Background sync limits\n\n- iOS gives no guaranteed background time: app refresh runs when the system\n  chooses, for about 30 seconds; silent pushes are throttled; background\n  `URLSession` handles transfers.\n- Android: WorkManager with a network constraint, periodic work at most\n  every 15 minutes, delayed by Doze and standby buckets.\n- expo-background-task and Flutter's workmanager plugin wrap the same\n  schedulers, with the same limits.\n- So sync on launch, on foreground, after each write and when connectivity\n  returns, and show the last sync time instead of promising \"always up to\n  date\".\n\n## 11. Large downloads\n\n- Resumable: HTTP range requests; a background `URLSession` with resume\n  data on iOS; WorkManager (long-running, with a notification) or\n  DownloadManager on Android.\n- Check free space before starting (iOS\n  `volumeAvailableCapacityForImportantUsage`, Android\n  `StorageManager.getAllocatableBytes`); on Wi-Fi when the user prefers or\n  the file is large; progress and Cancel visible.\n- Download to a temporary file, verify size or checksum, then move it into\n  place. Re-downloadable content goes where the system may purge it\n  (Caches on iOS), or is excluded from backup.\n\n## 12. Privacy\n\nKeep on the device only what offline features need, and purge old data on\na schedule (90 days of history, not everything ever). Encrypt what is\nsensitive, keep personal data out of logs and crash reports, and clear\neverything of the previous user on sign-out.\n\n## Check it\n\n- Airplane mode: cold launch, browse, create, edit, delete. Back online,\n  everything syncs once, with no duplicates.\n- A bad network: Network Link Conditioner on iOS (\"Very Bad Network\"),\n  `adb emu network speed gsm` and `adb emu network delay gprs` on the\n  emulator, or a proxy such as Proxyman or Charles. Timeouts and retries\n  behave as designed.\n- Kill the app mid-sync (swipe away, `adb shell am kill`), relaunch: no\n  lost writes and no doubles.\n- Two devices edit the same record: the policy holds and the user is told.\n- Install the previous store build, create data, install the new build\n  over it: the data is intact.\n- Sign out and in as another user: nothing of the first remains.\n\n## Avoid\n\nScreens that read straight from the network; writes lost when offline;\nretries without idempotency keys; device clocks deciding conflicts;\nsilent overwrites; reachability treated as truth; requests without\ntimeouts; tokens outside secure storage; destructive migrations over user\ndata; promises of background sync the OS will not keep; downloads that\nrestart from zero.\n","# Data on a phone that is often offline\n\nThe naive mobile data layer: every screen fetches when it opens and shows a\nspinner, nothing works on a train, a failed save is simply lost, a retry\ncreates a second order, two devices overwrite each other in silence, the\ntoken sits in plain preferences, an update's migration wipes the local\ndata, and a 2 GB download restarts from zero after every dropped\nconnection. This skill is the design that survives real networks. Stack\ndetails are in `mobile-ios`, `mobile-android`, `mobile-react-native` and\n`mobile-flutter`; the server side of the contract in `backend-api`.\n\n## 1. Decide how offline the app is\n\n| Level | Works offline | Build |\n|---|---|---|\n| Online with a cache | Recent reads; writes need a connection | HTTP cache or a persisted query cache; clear write errors |\n| Offline reads | Everything synced can be browsed; writes fail fast with Retry | A local database filled by sync |\n| Offline-first | Read and write everything; sync when possible | Local database as the truth, an outbox, a conflict policy, a server change feed |\n\n- Offline-first for apps used on the move: field work, inspections,\n  delivery, notes, travel, logs.\n- Some commits need the server at that moment (a payment, a booking against\n  stock): keep the user's intent locally and say plainly that it is not\n  done until confirmed.\n\n## 2. The local database is what the UI reads\n\n- The UI observes the local database (Room `Flow`, SwiftData `@Query`,\n  Drift `watch()`, WatermelonDB observables, Drizzle live queries on\n  expo-sqlite). Network responses are written into it; screens update from\n  it, never straight from a response.\n\n| Stack | Local store | Sync engines to consider |\n|---|---|---|\n| Android | Room | PowerSync, Firestore, Couchbase Lite |\n| iOS | SwiftData, Core Data, GRDB | CloudKit (private data), PowerSync |\n| Flutter | Drift | PowerSync, Firestore |\n| React Native | expo-sqlite with Drizzle, op-sqlite, WatermelonDB | PowerSync, WatermelonDB's sync protocol |\n\n- Realm's Atlas Device Sync reached end of life in September 2025: do not\n  start on it; plan the move where it is still in use.\n- Ids generated on the device (UUID v7, time-ordered, or v4), so records\n  created offline have stable ids before the server sees them.\n- Ordering comes from the server (a version or its timestamp), never from\n  the device clock, which users and time zones change.\n\n## 3. Pull: changes since a cursor\n\n```\nGET \u002Fsync\u002Forders?cursor=eyJ2IjoxMjM0fQ&limit=500\n200 { \"changes\": [{ \"id\": \"0192…\", \"version\": 1235, \"deletedAt\": null, \"status\": \"paid\" }],\n      \"cursor\": \"eyJ2IjoxMjM1fQ\", \"hasMore\": false }\n```\n\n- The server stamps each change with an increasing version (a sequence set\n  in the same transaction as the write); the cursor is opaque.\n- Deletes are tombstones (`deletedAt`), kept for a set window (30 to 90\n  days). A client whose cursor is older than the window does a full resync.\n- Apply each page and its new cursor in one local transaction, so a crash\n  never keeps one without the other. Page until `hasMore` is false.\n- Pull on launch and foreground, after a push, when a push notification\n  says something changed, and in background work when the OS allows.\n- Sync only what the user needs (their records, recent months).\n\n## 4. Push: an outbox with idempotency keys\n\n- Each local write is one transaction: the row changes (marked pending) and\n  an outbox row is added: entity, operation, payload, idempotency key,\n  attempts, next attempt time, last error.\n- One sender drains the outbox in order. The idempotency key is created\n  once, when the user acted, and sent on every retry\n  (`Idempotency-Key`); the server returns the stored result for a repeat.\n- Success applies the server's version to the row and removes the outbox\n  entry. Transient failures (offline, timeout, 5xx, 429) back off. Permanent\n  ones (validation, 403, an unresolved 409) mark the item failed and show\n  it to the user with fix or discard, never retried forever.\n- Parents before children: a record created offline that references\n  another created offline waits for it.\n\n```ts\nexport async function drainOutbox(db: Db, api: Api, signal: AbortSignal) {\n  for (const item of await db.outbox.due(Date.now())) {\n    if (signal.aborted) return;\n    try {\n      const saved = await api.send(item.op, item.payload, { idempotencyKey: item.key, signal });\n      await db.transaction(async (tx) => {\n        await tx.records.applyServerVersion(saved);\n        await tx.outbox.remove(item.id);\n      });\n    } catch (e) {\n      if (isPermanent(e)) await db.outbox.markFailed(item.id, errorCode(e));\n      else await db.outbox.reschedule(item.id, Date.now() + backoffMs(item.attempts));\n      if (isOffline(e)) return; \u002F\u002F wait for connectivity instead of failing every item\n    }\n  }\n}\n```\n\n## 5. Conflicts\n\n| Policy | Use for | Cost |\n|---|---|---|\n| Server authority: stale versions get `409` | Money, stock, bookings, anything with rules | The user reloads and redoes the change |\n| Last write wins, in the server's order | One person's data across their devices, low-stakes fields | Concurrent edits are lost silently |\n| Field-level merge against a base version | Profiles, forms, records several people edit | More server logic |\n| CRDTs (Yjs, Automerge) | Collaborative text and lists edited offline | Complexity, growing history |\n\n- Every update carries the base `version` it was made from; the server\n  compares, applies the policy, and returns the current record on conflict.\n- Tell the user when something could not sync: a visible state on the item\n  (\"Not synced\", with Retry and Discard), never a silent drop.\n\n## 6. Network reality\n\n- Timeouts on every request: about 10 seconds to connect, 15 to 30 seconds\n  in total for an API call, longer for uploads by size. Defaults are wrong:\n  URLSession waits 60 seconds, `fetch` and Dart's `http` never time out.\n- Retry only idempotent requests (GET, PUT, DELETE, and POST with an\n  idempotency key), with exponential backoff and full jitter, capped:\n\n```ts\nconst backoffMs = (attempt: number) => Math.random() * Math.min(60_000, 1_000 * 2 ** attempt);\n```\n\n- Honour `Retry-After` on `429` and `503`; stop after about 5 attempts in\n  the foreground and leave the rest to the outbox.\n- Reachability (`NWPathMonitor`, `ConnectivityManager`, NetInfo,\n  connectivity_plus) is a hint: a connected network may be a captive\n  portal. Always try and handle failure; use it to resume sync and show\n  the offline banner.\n- Fewer round trips: one sync call with many changes, pages of 50 to 500\n  rows, only the fields needed, compressed responses (the platform clients\n  decode gzip for you). Cancel requests when their screen leaves.\n- On metered or Low Data connections (`isActiveNetworkMetered`,\n  `NWPath.isConstrained`), defer large optional downloads.\n\n## 7. Caching\n\n- Images through the platform or library cache (Coil, Nuke, Kingfisher,\n  expo-image, cached_network_image), from URLs that change when the content\n  does, so caches never go stale.\n- HTTP caching: `Cache-Control` and `ETag` with `If-None-Match` turn repeat\n  loads into `304`s; OkHttp needs a `Cache` configured to use them.\n- TanStack Query in React Native: persist the cache with a `maxAge` (24\n  hours) and a `gcTime` at least as long, and resume paused mutations after\n  the restore (`onSuccess={() => queryClient.resumePausedMutations()}` on\n  `PersistQueryClientProvider`).\n- Show cached data at once with its age (\"Updated 5 min ago\") and refresh\n  behind it.\n\n## 8. Secure storage\n\n- Tokens in the Keychain or Keystore-backed storage (expo-secure-store,\n  flutter_secure_storage, `mobile-ios`, `mobile-android`); never\n  AsyncStorage, UserDefaults, SharedPreferences, plain SQLite, logs or\n  crash reports.\n- The access token short-lived and in memory; the refresh token in secure\n  storage; one refresh in flight at a time.\n- Sign-out clears tokens, the user's local data, caches and the outbox\n  (warn first when changes are unsynced: \"3 changes are not synced yet\"),\n  and unregisters the push token.\n- Sensitive databases encrypted with SQLCipher (op-sqlite, GRDB, Room with a\n  SQLCipher factory), the key in the Keychain or Keystore; iOS files with\n  complete data protection.\n\n## 9. Local schema migrations\n\n- The schema has a version; migrations run in order from any shipped\n  version to the current one. Test upgrading a real database file from\n  every version still installed (Room `MigrationTestHelper`, Drift schema\n  tests, SwiftData migration plans).\n- Adding is cheap; renames and type changes copy through a new table. A\n  destructive fallback only for caches that can be downloaded again.\n\n```ts\nconst row = await db.getFirstAsync\u003C{ user_version: number }>(\"PRAGMA user_version\");\nconst current = row?.user_version ?? 0;\nfor (const [version, sql] of MIGRATIONS) { \u002F\u002F [[1, \"CREATE TABLE ...\"], [2, \"ALTER TABLE ...\"]]\n  if (version \u003C= current) continue;\n  await db.withTransactionAsync(async () => {\n    await db.execAsync(sql);\n    await db.execAsync(`PRAGMA user_version = ${version}`);\n  });\n}\n```\n\n- Old app versions stay installed for months: the sync API accepts them, or\n  the app checks a minimum supported version at launch and shows \"Update\n  required\".\n\n## 10. Background sync limits\n\n- iOS gives no guaranteed background time: app refresh runs when the system\n  chooses, for about 30 seconds; silent pushes are throttled; background\n  `URLSession` handles transfers.\n- Android: WorkManager with a network constraint, periodic work at most\n  every 15 minutes, delayed by Doze and standby buckets.\n- expo-background-task and Flutter's workmanager plugin wrap the same\n  schedulers, with the same limits.\n- So sync on launch, on foreground, after each write and when connectivity\n  returns, and show the last sync time instead of promising \"always up to\n  date\".\n\n## 11. Large downloads\n\n- Resumable: HTTP range requests; a background `URLSession` with resume\n  data on iOS; WorkManager (long-running, with a notification) or\n  DownloadManager on Android.\n- Check free space before starting (iOS\n  `volumeAvailableCapacityForImportantUsage`, Android\n  `StorageManager.getAllocatableBytes`); on Wi-Fi when the user prefers or\n  the file is large; progress and Cancel visible.\n- Download to a temporary file, verify size or checksum, then move it into\n  place. Re-downloadable content goes where the system may purge it\n  (Caches on iOS), or is excluded from backup.\n\n## 12. Privacy\n\nKeep on the device only what offline features need, and purge old data on\na schedule (90 days of history, not everything ever). Encrypt what is\nsensitive, keep personal data out of logs and crash reports, and clear\neverything of the previous user on sign-out.\n\n## Check it\n\n- Airplane mode: cold launch, browse, create, edit, delete. Back online,\n  everything syncs once, with no duplicates.\n- A bad network: Network Link Conditioner on iOS (\"Very Bad Network\"),\n  `adb emu network speed gsm` and `adb emu network delay gprs` on the\n  emulator, or a proxy such as Proxyman or Charles. Timeouts and retries\n  behave as designed.\n- Kill the app mid-sync (swipe away, `adb shell am kill`), relaunch: no\n  lost writes and no doubles.\n- Two devices edit the same record: the policy holds and the user is told.\n- Install the previous store build, create data, install the new build\n  over it: the data is intact.\n- Sign out and in as another user: nothing of the first remains.\n\n## Avoid\n\nScreens that read straight from the network; writes lost when offline;\nretries without idempotency keys; device clocks deciding conflicts;\nsilent overwrites; reachability treated as truth; requests without\ntimeouts; tokens outside secure storage; destructive migrations over user\ndata; promises of background sync the OS will not keep; downloads that\nrestart from zero.\n",{"name":5,"description":7},"main","38293fe1b096e6bf3646c7344c38786ed688f464","crates\u002Fenowx-core\u002Fskills\u002Fmobile-data",21,"https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli","https:\u002F\u002Fenowx.ai","https:\u002F\u002Fgithub.com\u002Fenowdev","Pretending to be a Developer","2026-10-06 02:05:35",{"source":8,"skillName":5,"commands":64,"prompt":74,"zipAvailable":75},[65,68,71],{"tool":66,"command":67},"npx","npx skills add enowdev\u002Fenowxcli --skill mobile-data",{"tool":69,"command":70},"bunx","bunx skills add enowdev\u002Fenowxcli --skill mobile-data",{"tool":72,"command":73},"pnpm","pnpm dlx skills add enowdev\u002Fenowxcli --skill mobile-data","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fmobile-data\u002FSKILL.md",true,[77,83,87,91,95,99],{"path":78,"resourceType":79,"resourceTypeDesc":80,"sizeBytes":81,"sourceHtmlUrl":82},"references\u002Fcompeting-risks.md","REFERENCE","参考文档",11986,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fscikit-survival\u002Freferences\u002Fcompeting-risks.md",{"path":84,"resourceType":79,"resourceTypeDesc":80,"sizeBytes":85,"sourceHtmlUrl":86},"references\u002Fcox-models.md",5238,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fscikit-survival\u002Freferences\u002Fcox-models.md",{"path":88,"resourceType":79,"resourceTypeDesc":80,"sizeBytes":89,"sourceHtmlUrl":90},"references\u002Fdata-handling.md",12094,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fscikit-survival\u002Freferences\u002Fdata-handling.md",{"path":92,"resourceType":79,"resourceTypeDesc":80,"sizeBytes":93,"sourceHtmlUrl":94},"references\u002Fensemble-models.md",9529,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fscikit-survival\u002Freferences\u002Fensemble-models.md",{"path":96,"resourceType":79,"resourceTypeDesc":80,"sizeBytes":97,"sourceHtmlUrl":98},"references\u002Fevaluation-metrics.md",10850,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fscikit-survival\u002Freferences\u002Fevaluation-metrics.md",{"path":100,"resourceType":79,"resourceTypeDesc":80,"sizeBytes":101,"sourceHtmlUrl":102},"references\u002Fsvm-models.md",11096,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fscikit-survival\u002Freferences\u002Fsvm-models.md",[104,118,141,156,182,204],{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":113,"publishedAt":49},"01M6D2HQYSQD9N7MTNDT4A7CBM","demo-skill","Project-scope demo skill for tests.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fworkspace\u002F.agents\u002Fskills\u002Fdemo-skill\u002FSKILL.md",[110],{"categoryNo":111,"categoryName":112,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[114,115,116,117,39],"demo skill","demo","tests","project",{"skillNo":119,"name":120,"nameZh":6,"description":121,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":122,"categories":123,"keywords":130,"publishedAt":49},"01M6D2HM0GCPM0TM2HPG9HHR2R","writing","Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fwriting\u002FSKILL.md",[124,127],{"categoryNo":125,"categoryName":126,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":128,"categoryName":129,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000036","内容创作者",[120,131,132,133,134,135,39,136,137,138,139,140],"text","emails","empty","drop","document","page","voice","notifications","documentation","translate",{"skillNo":142,"name":143,"nameZh":6,"description":144,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":150,"publishedAt":49},"01M6D2HG07YS49NM196Q0R3AZ8","ui","Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui\u002FSKILL.md",[147],{"categoryNo":148,"categoryName":149,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000032","设计师",[143,151,152,153,154,136,155,39,32],"icons","components","SET","responsive","layout",{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":168,"publishedAt":49},"01M6D2HB59HZHWG1RYZDV4SJ91","ui-themes","Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-themes\u002FSKILL.md",[162,165],{"categoryNo":163,"categoryName":164,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":166,"categoryName":167,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师",[143,169,170,171,33,172,173,174,175,39,32,176,136,177,178,179,151,180,181],"ui themes","switching","photos","drawing","focus","maps","Art","flash","code","Charts","content","screenshots","logo",{"skillNo":183,"name":184,"nameZh":6,"description":185,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":186,"categories":187,"keywords":191,"publishedAt":49},"01M6D2H5R5KRAE524C8QX0T7Q0","ui-structure","Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-structure\u002FSKILL.md",[188,189,190],{"categoryNo":125,"categoryName":126,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":166,"categoryName":167,"taxonomyType":26,"taxonomyTypeDesc":27},{"categoryNo":128,"categoryName":129,"taxonomyType":26,"taxonomyTypeDesc":27},[192,143,193,152,194,195,177,196,197,198,155,33,199,200,201,39,32,136,202,203],"structure","ui structure","pricing","dashboard","Blueprint","article","anatomy","styling","primitives","list","why","how",{"skillNo":205,"name":206,"nameZh":6,"description":207,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":208,"categories":209,"keywords":217,"publishedAt":49},"01M6D2H0ZZBJG77MBGPZHA6MWD","ui-stack-vue","Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-stack-vue\u002FSKILL.md",[210,213,216],{"categoryNo":211,"categoryName":212,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":214,"categoryName":215,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":148,"categoryName":149,"taxonomyType":26,"taxonomyTypeDesc":27},[143,218,219,220,221,222,223,224,155,117,225,226,39,227,228,229,42,230,231,232],"vue","stack","ui stack vue","forms","pages","watch","runtime","transitions","fetch","pinia","nuxt","template","config","directory","testing"]