Room is the structured local source. DataStore is preferences/proto — not a second SQLite. App: android-development. Streams: kotlin-coroutines. JVM tests: android-unit-testing.
Stack and scope
| Item | Role |
|---|---|
| Room | SQLite, DAOs, migrations |
| KSP | Annotation processing |
| DataStore | Preferences or Proto |
| SQLCipher | Only if the product requires encryption |
Do not introduce Realm/ObjectBox into a feature if Room is already in the app.
Organization
data/local/
db/
AppDatabase.kt
entities/
daos/
converters/
migrations/
datastore/
UserPrefs.kt
- One
AppDatabase. DAOs per aggregate. - Repository combines Room + network; UI does not call DAO.
Non-negotiable rules (Room)
exportSchema = trueand schema in git. Migrations tested.- Never destructive
fallbackToDestructiveMigration()in production. - Queries in the DAO; no SQL concatenated with input.
Flow/PagingSourcefor observation.suspendfor writes.- Main thread:
allowMainThreadQueriesforbidden in prod. - Version + migration for every schema change.
@Dao
interface OrderDao {
@Query("SELECT * FROM orders WHERE id = :id")
fun observe(id: String): Flow<OrderEntity?>
@Upsert
suspend fun upsert(order: OrderEntity)
}
Modeling
- Entities ≠ UI models. Map in the data layer.
@Embedded/@Relationaware (relation N+1).- Indexes and
uniquealigned with real queries. - TypeConverters for enums/instant; not for opaque JSON graphs without need.
- Foreign keys with explicit
onDelete.
Migrations
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE orders ADD COLUMN notes TEXT")
}
}
MigrationTestHelperin JVM tests.- New columns nullable or with default. Backfill in migration if needed.
- AutoMigration only when the diff is trivial and reviewed.
DataStore
- Typed keys; one file per context (
settings,session). edit { }atomic. NoSharedPreferencesnew.- Proto DataStore for versioned schemas; Preferences for simple flags.
- Do not put large lists / business blobs in DataStore — use Room.
- Read with Flow;
first()only on one-off boot.
Concurrency and quality
- Writes serialized on the Room executor. Do not open multiple
AppDatabase. - Tests:
Room.inMemoryDatabaseBuilder+InstantTaskExecutor/runTest. - Paging 3 for infinite lists.
Anti-patterns
getAll()in theonCreateof the Activity- Entity in Compose UI
- Destructive migration “just this once”
- DataStore as feed cache
- Gson converter for a giant object in a column
Integration with other skills
- App:
android-development - Flow:
kotlin-coroutines - Tests:
android-unit-testing - Offline + network: single repository, not two sources in the UI
Completion criteria
- DAO + Flow; no SQL in the UI
- Versioned and tested migration if the schema changed
- Exported schema
- Prefs in the DataStore, not the new SP
- No queries in Main