agentleFS
wrinfotelwrinfotel/verified-android-skills/skills/offline-first/SKILL.md

offline-first

Skill0 starsChanged 32 days agoMITView on GitHub

Offline-first Android data discipline: the local source as the single source of truth, repositories as the only data-layer entry point, Room without main-thread access, migrations instead of destructive fallback, and one DataStore instance per file. Use when writing or reviewing repositories, Room databases, DataStore delegates, or sync workers. Core rules have machine checks in checks/.

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 5 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    After ANY data-layer edit, run the Verification step (bottom of this file).

    line 16
  2. Distinctive:

    MUST structure offline-first reads around a local source of truth (a database): repositories expose data as observable types (Flow) read …

    line 20
  3. Distinctive:

    MUST NOT let state holders touch data sources directly: other layers "should not access data sources directly.

    line 26
  4. Very specific:

    MUST NOT call allowMainThreadQueries(): "To prevent queries from blocking the UI, Room does not support database access on the main thread" …

    line 56
  5. Very specific:

    MUST NOT ship fallbackToDestructiveMigration(): when a migration is needed and no path is defined, Room "permanently deletes all data from …

    line 78
  6. Distinctive:

    MUST NOT create more than one DataStore instance for a given file in the same process: "Doing so can break all DataStore functionality...

    line 108
  7. Distinctive:

    Run after every data-layer edit (from the project root):

    line 147
  8. Very specific:

    All four must pass (exit 0) before declaring the task done.

    line 156

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Offline-First

Operational rules for a coding agent. RIGHT/WRONG pairs are canonical. After ANY data-layer edit, run the Verification step (bottom of this file).

1. The local source is the single source of truth

MUST structure offline-first reads around a local source of truth (a database): repositories expose data as observable types (Flow) read from the local source, and writes go to the local source first, then sync to the network. "An offline-first app must be able to perform read operations without network access."

MUST NOT let state holders touch data sources directly: other layers "should not access data sources directly. The entry points to the data layer are always repository classes" — state holders (ViewModels) "should never have a direct dependency on a data source". The domain and UI layers must never communicate directly with the network layer.

WRONG:

@HiltViewModel
class FeedViewModel @Inject constructor() : ViewModel() {
    private val api = Retrofit.Builder()
        .baseUrl("https://example.com/")
        .build()                          // network source in a state holder
}

RIGHT:

@HiltViewModel
class FeedViewModel @Inject constructor(
    private val feedRepository: FeedRepository,   // Flow from the local DB
) : ViewModel()

The check flags data-source imports (Retrofit, OkHttp, Ktor, Room, DataStore) inside ViewModel files. Repositories are exempt — they ARE the data layer.

2. Room never runs on the main thread

MUST NOT call allowMainThreadQueries(): "To prevent queries from blocking the UI, Room does not support database access on the main thread" — the builder escape hatch reintroduces exactly the jank and ANR Room exists to prevent. DAOs expose suspend functions for one-shot operations and Flow for observable queries, executed from coroutines.

WRONG:

Room.databaseBuilder<AppDb>(context, "app.db")
    .allowMainThreadQueries()
    .build()

RIGHT:

Room.databaseBuilder<AppDb>(context, "app.db").build()
// @Dao: suspend fun loadUserById(id: Int): User
//       fun loadUsers(): Flow<List<User>>

3. Migrations, not destruction

MUST NOT ship fallbackToDestructiveMigration(): when a migration is needed and no path is defined, Room "permanently deletes all data from the app's database tables" — silent user-data loss on every schema bump. Provide Migration(startVersion, endVersion) objects via addMigrations(...). The targeted variants (fallbackToDestructiveMigrationFrom / ...OnDowngrade) are the documented tools for deliberately scoped rebuilds — the check flags only the blanket option.

WRONG:

Room.databaseBuilder<AppDb>(context, "app.db")
    .fallbackToDestructiveMigration()
    .build()

RIGHT:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}
Room.databaseBuilder<AppDb>(context, "app.db")
    .addMigrations(MIGRATION_1_2)
    .build()

4. One DataStore instance per file

MUST NOT create more than one DataStore instance for a given file in the same process: "Doing so can break all DataStore functionality... DataStore will throw IllegalStateException when reading or updating data." Each preferencesDataStore(name = ...) delegate maps to a file — names must be unique across the app.

WRONG:

// SettingsStore.kt
val Context.settingsStore by preferencesDataStore(name = "settings")
// ProfileStore.kt
val Context.profileStore by preferencesDataStore(name = "settings")  // same file!

RIGHT:

// SettingsStore.kt
val Context.settingsStore by preferencesDataStore(name = "settings")
// ThemeStore.kt
val Context.themeStore by preferencesDataStore(name = "theme")

DataStore is for small datasets: "If you need to support large or complex datasets, partial updates, or referential integrity, consider using Room instead."

5. What the checks cannot see (review-verified)

  • Sync strategy: queued/lazy writes with WorkManager ("persistent work" is what it is for) — unique work with ExistingWorkPolicy.KEEP, a network constraint, and Result.retry() for automatic exponential backoff.
  • Conflict resolution (last-write-wins timestamps) and LCE UI states (Loading / Content / Error) around offline reads.
  • SharedPreferences are legacy: "If you're using SharedPreferences to store data, consider migrating to DataStore instead."

Verification

Run after every data-layer edit (from the project root):

bash <skill-dir>/checks/check_no_data_source_in_viewmodel.sh <kotlin-src-dir>
bash <skill-dir>/checks/check_room_allow_main_thread.sh      <kotlin-src-dir>
bash <skill-dir>/checks/check_room_fallback_destructive.sh   <kotlin-src-dir>
bash <skill-dir>/checks/check_datastore_dup_names.sh         <kotlin-src-dir>

All four must pass (exit 0) before declaring the task done. Checks are deterministic heuristics — they catch the documented anti-patterns above, not every offline-first bug. §5 is verified by review against references/offline-first.md.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.