@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/docs/facts.json +6 -6
  3. package/manifest.json +55 -34
  4. package/package.json +1 -1
  5. package/pipeline/multi-agent-refs/features/usage-reporting.md +7 -3
  6. package/pipeline/schemas/prefs.schema.json +4 -0
  7. package/pipeline/scripts/usage-register.mjs +2 -0
  8. package/pipeline/skills/.skill-manifest.json +36 -20
  9. package/pipeline/skills/.skills-index.json +75 -9
  10. package/pipeline/skills/shared/README.md +13 -7
  11. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  12. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  13. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  14. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  15. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  16. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  17. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  18. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  19. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  20. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  21. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  22. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  23. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  24. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  25. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  26. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  27. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  28. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  29. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  30. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  31. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  32. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  33. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  34. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  35. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  36. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  37. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  38. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  39. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  40. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  41. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  42. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  43. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  44. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  45. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  46. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  47. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  48. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  49. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  50. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  51. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  52. package/pipeline/skills/skills-index.md +8 -2
@@ -8,6 +8,11 @@ description: "Implement Room database persistence on Android with @Entity, @Dao,
8
8
  Room persistence library patterns for Android targeting Room 2.6+ with
9
9
  KSP, Kotlin coroutines, and Flow-based reactive queries.
10
10
 
11
+ Detailed, copy-ready code for every section below lives in
12
+ [references/patterns.md](references/patterns.md). Load that file when you need a
13
+ full example to adapt (entity, DAO, migration, Hilt module, or a test). Each
14
+ heading there mirrors a section here.
15
+
11
16
  ## Contents
12
17
 
13
18
  - [Entity Definition](#entity-definition)
@@ -18,6 +23,9 @@ KSP, Kotlin coroutines, and Flow-based reactive queries.
18
23
  - [Relations](#relations)
19
24
  - [Migration Strategies](#migration-strategies)
20
25
  - [Hilt Integration](#hilt-integration)
26
+ - [Encrypted Database (SQLCipher)](#encrypted-database-sqlcipher)
27
+ - [Async Database Provisioning](#async-database-provisioning)
28
+ - [Runtime Hygiene](#runtime-hygiene)
21
29
  - [Testing with In-Memory Database](#testing-with-in-memory-database)
22
30
  - [Do's and Don'ts](#dos-and-donts)
23
31
  - [Troubleshooting](#troubleshooting)
@@ -25,107 +33,22 @@ KSP, Kotlin coroutines, and Flow-based reactive queries.
25
33
 
26
34
  ## Entity Definition
27
35
 
28
- ```kotlin
29
- @Entity(
30
- tableName = "flights",
31
- indices = [
32
- Index(value = ["origin_code", "destination_code"]),
33
- Index(value = ["departure_time"]),
34
- ],
35
- )
36
- data class FlightEntity(
37
- @PrimaryKey
38
- val id: String,
39
- @ColumnInfo(name = "origin_code")
40
- val originCode: String,
41
- @ColumnInfo(name = "origin_name")
42
- val originName: String,
43
- @ColumnInfo(name = "destination_code")
44
- val destinationCode: String,
45
- @ColumnInfo(name = "destination_name")
46
- val destinationName: String,
47
- @ColumnInfo(name = "departure_time")
48
- val departureTime: String,
49
- @ColumnInfo(name = "arrival_time")
50
- val arrivalTime: String,
51
- @ColumnInfo(name = "price_amount")
52
- val priceAmount: Double,
53
- @ColumnInfo(name = "price_currency")
54
- val priceCurrency: String,
55
- val status: String,
56
- @ColumnInfo(name = "updated_at", defaultValue = "0")
57
- val updatedAt: Long = System.currentTimeMillis(),
58
- )
59
- ```
60
-
61
- ### Composite Primary Key
36
+ Annotate a `data class` with `@Entity(tableName = ...)`. Declare a `@PrimaryKey`,
37
+ map snake_case columns with `@ColumnInfo(name = ...)`, and add `indices` for
38
+ columns that appear in `WHERE`/`ORDER BY`. Use `primaryKeys = [...]` on the
39
+ entity for a composite key instead of `@PrimaryKey`.
62
40
 
63
- ```kotlin
64
- @Entity(
65
- tableName = "booking_passengers",
66
- primaryKeys = ["booking_id", "passenger_id"],
67
- )
68
- data class BookingPassengerEntity(
69
- @ColumnInfo(name = "booking_id")
70
- val bookingId: String,
71
- @ColumnInfo(name = "passenger_id")
72
- val passengerId: String,
73
- @ColumnInfo(name = "seat_number")
74
- val seatNumber: String?,
75
- )
76
- ```
41
+ See [references/patterns.md#entity-definition](references/patterns.md#entity-definition)
42
+ for a full entity and a composite-key entity.
77
43
 
78
44
  ## DAO Interface
79
45
 
80
- ```kotlin
81
- @Dao
82
- interface FlightDao {
83
-
84
- @Query("SELECT * FROM flights WHERE origin_code = :origin AND destination_code = :destination ORDER BY departure_time ASC")
85
- fun getFlights(origin: String, destination: String): Flow<List<FlightEntity>>
86
-
87
- @Query("SELECT * FROM flights WHERE id = :id")
88
- fun getFlightById(id: String): Flow<FlightEntity?>
89
-
90
- @Query("SELECT * FROM flights WHERE id = :id")
91
- suspend fun getFlightByIdOnce(id: String): FlightEntity?
92
-
93
- @Insert(onConflict = OnConflictStrategy.REPLACE)
94
- suspend fun insertFlight(flight: FlightEntity)
46
+ Mark the interface `@Dao`. Return `Flow<T>` for reactive reads and `suspend`
47
+ for one-shot reads/writes. Use `@Query`, `@Insert`, `@Update`, `@Delete`,
48
+ `@Upsert`, and wrap multi-statement operations in a `@Transaction` default
49
+ method.
95
50
 
96
- @Insert(onConflict = OnConflictStrategy.REPLACE)
97
- suspend fun insertFlights(flights: List<FlightEntity>)
98
-
99
- @Upsert
100
- suspend fun upsertFlights(flights: List<FlightEntity>)
101
-
102
- @Update
103
- suspend fun updateFlight(flight: FlightEntity)
104
-
105
- @Delete
106
- suspend fun deleteFlight(flight: FlightEntity)
107
-
108
- @Query("DELETE FROM flights WHERE id = :id")
109
- suspend fun deleteFlightById(id: String)
110
-
111
- @Query("DELETE FROM flights WHERE updated_at < :threshold")
112
- suspend fun deleteStaleFlights(threshold: Long)
113
-
114
- @Query("SELECT COUNT(*) FROM flights")
115
- fun getFlightCount(): Flow<Int>
116
-
117
- @Transaction
118
- suspend fun replaceAllFlights(flights: List<FlightEntity>) {
119
- deleteAllFlights()
120
- insertFlights(flights)
121
- }
122
-
123
- @Query("DELETE FROM flights")
124
- suspend fun deleteAllFlights()
125
- }
126
- ```
127
-
128
- ### @Upsert vs @Insert(onConflict = REPLACE)
51
+ Pick the write strategy by intent:
129
52
 
130
53
  | Strategy | Behavior | Use When |
131
54
  |----------|----------|----------|
@@ -133,30 +56,13 @@ interface FlightDao {
133
56
  | `@Insert(REPLACE)` | Delete + re-insert on conflict | Simpler; triggers delete cascade |
134
57
  | `@Insert(IGNORE)` | Skip on conflict | Inserting if not exists, no update needed |
135
58
 
136
- ## Database Setup
59
+ Full DAO surface: [references/patterns.md#dao-interface](references/patterns.md#dao-interface).
137
60
 
138
- ```kotlin
139
- @Database(
140
- entities = [
141
- FlightEntity::class,
142
- BookingEntity::class,
143
- BookingPassengerEntity::class,
144
- ],
145
- version = 2,
146
- autoMigrations = [
147
- AutoMigration(from = 1, to = 2),
148
- ],
149
- exportSchema = true,
150
- )
151
- @TypeConverters(Converters::class)
152
- abstract class AppDatabase : RoomDatabase() {
153
- abstract fun flightDao(): FlightDao
154
- abstract fun bookingDao(): BookingDao
155
- }
156
- ```
61
+ ## Database Setup
157
62
 
158
- Always set `exportSchema = true` and configure the schema export directory in
159
- `build.gradle.kts`:
63
+ Declare an `abstract class ... : RoomDatabase()` with `@Database(entities, version)`,
64
+ one abstract accessor per DAO, and `@TypeConverters` when needed. Always set
65
+ `exportSchema = true` and point KSP at a schema directory:
160
66
 
161
67
  ```kotlin
162
68
  ksp {
@@ -164,357 +70,103 @@ ksp {
164
70
  }
165
71
  ```
166
72
 
167
- ## Reactive Queries with Flow
168
-
169
- DAO methods returning `Flow` automatically emit new values when the underlying
170
- table changes.
73
+ Full `@Database` declaration:
74
+ [references/patterns.md#database-setup](references/patterns.md#database-setup).
171
75
 
172
- ```kotlin
173
- // DAO returns Flow
174
- @Query("SELECT * FROM flights WHERE origin_code = :origin")
175
- fun getFlightsByOrigin(origin: String): Flow<List<FlightEntity>>
176
-
177
- // Repository maps to domain models
178
- class FlightRepositoryImpl @Inject constructor(
179
- private val dao: FlightDao,
180
- private val mapper: FlightMapper,
181
- ) : FlightRepository {
182
-
183
- override fun getFlights(origin: String): Flow<List<Flight>> =
184
- dao.getFlightsByOrigin(origin)
185
- .map { entities -> entities.map(mapper::toDomain) }
186
- .flowOn(Dispatchers.IO)
187
- }
76
+ ## Reactive Queries with Flow
188
77
 
189
- // ViewModel collects in viewModelScope
190
- @HiltViewModel
191
- class FlightListViewModel @Inject constructor(
192
- private val repository: FlightRepository,
193
- ) : ViewModel() {
194
-
195
- val flights: StateFlow<List<Flight>> = repository.getFlights("IST")
196
- .stateIn(
197
- scope = viewModelScope,
198
- started = SharingStarted.WhileSubscribed(5_000),
199
- initialValue = emptyList(),
200
- )
201
- }
78
+ A DAO method returning `Flow` re-emits whenever its table changes. Map entities
79
+ to domain models in the repository (`.map { ... }.flowOn(Dispatchers.IO)`),
80
+ expose a `StateFlow` from the ViewModel via `stateIn(..., WhileSubscribed(5_000),
81
+ ...)`, and collect with `collectAsStateWithLifecycle()` in Compose.
202
82
 
203
- // Compose collects as state
204
- @Composable
205
- fun FlightListScreen(viewModel: FlightListViewModel = hiltViewModel()) {
206
- val flights by viewModel.flights.collectAsStateWithLifecycle()
207
- LazyColumn {
208
- items(flights, key = { it.id }) { flight ->
209
- FlightCard(flight = flight)
210
- }
211
- }
212
- }
213
- ```
83
+ Full repository -> ViewModel -> Compose chain:
84
+ [references/patterns.md#reactive-queries-with-flow](references/patterns.md#reactive-queries-with-flow).
214
85
 
215
86
  ## TypeConverters
216
87
 
217
- For types Room does not natively support (lists, enums, dates).
218
-
219
- ```kotlin
220
- class Converters {
221
-
222
- private val json = Json { ignoreUnknownKeys = true }
223
-
224
- @TypeConverter
225
- fun fromStringList(value: List<String>): String =
226
- json.encodeToString(value)
227
-
228
- @TypeConverter
229
- fun toStringList(value: String): List<String> =
230
- json.decodeFromString(value)
231
-
232
- @TypeConverter
233
- fun fromInstant(value: Instant?): Long? = value?.toEpochMilliseconds()
88
+ For types Room does not natively support (lists, enums, dates), write a
89
+ `Converters` class with paired `@TypeConverter` functions and register it with
90
+ `@TypeConverters(Converters::class)` on the `@Database` (or a specific entity/DAO).
234
91
 
235
- @TypeConverter
236
- fun toInstant(value: Long?): Instant? = value?.let { Instant.fromEpochMilliseconds(it) }
237
-
238
- @TypeConverter
239
- fun fromFlightStatus(value: FlightStatus): String = value.name
240
-
241
- @TypeConverter
242
- fun toFlightStatus(value: String): FlightStatus = FlightStatus.valueOf(value)
243
- }
244
- ```
245
-
246
- Register in `@Database`:
247
-
248
- ```kotlin
249
- @Database(entities = [...], version = 1)
250
- @TypeConverters(Converters::class)
251
- abstract class AppDatabase : RoomDatabase()
252
- ```
92
+ Full converter set (list, `Instant`, enum):
93
+ [references/patterns.md#typeconverters](references/patterns.md#typeconverters).
253
94
 
254
95
  ## Relations
255
96
 
256
- ### @Embedded
257
-
258
- Flatten a nested object into the parent table columns:
259
-
260
- ```kotlin
261
- data class AddressEntity(
262
- val street: String,
263
- val city: String,
264
- val country: String,
265
- )
266
-
267
- @Entity(tableName = "passengers")
268
- data class PassengerEntity(
269
- @PrimaryKey val id: String,
270
- val name: String,
271
- @Embedded(prefix = "address_")
272
- val address: AddressEntity,
273
- )
274
- // Creates columns: id, name, address_street, address_city, address_country
275
- ```
276
-
277
- ### @Relation (One-to-Many)
278
-
279
- ```kotlin
280
- data class BookingWithPassengers(
281
- @Embedded val booking: BookingEntity,
282
- @Relation(
283
- parentColumn = "id",
284
- entityColumn = "booking_id",
285
- )
286
- val passengers: List<BookingPassengerEntity>,
287
- )
288
-
289
- @Dao
290
- interface BookingDao {
291
- @Transaction
292
- @Query("SELECT * FROM bookings WHERE id = :bookingId")
293
- fun getBookingWithPassengers(bookingId: String): Flow<BookingWithPassengers?>
294
- }
295
- ```
296
-
297
- ### Junction Table (Many-to-Many)
298
-
299
- ```kotlin
300
- @Entity(
301
- tableName = "flight_tags",
302
- primaryKeys = ["flight_id", "tag_id"],
303
- )
304
- data class FlightTagCrossRef(
305
- @ColumnInfo(name = "flight_id") val flightId: String,
306
- @ColumnInfo(name = "tag_id") val tagId: String,
307
- )
308
-
309
- data class FlightWithTags(
310
- @Embedded val flight: FlightEntity,
311
- @Relation(
312
- parentColumn = "id",
313
- entityColumn = "id",
314
- associateBy = Junction(
315
- value = FlightTagCrossRef::class,
316
- parentColumn = "flight_id",
317
- entityColumn = "tag_id",
318
- ),
319
- )
320
- val tags: List<TagEntity>,
321
- )
322
- ```
97
+ - `@Embedded(prefix = ...)` flattens a nested object into the parent table.
98
+ - `@Relation(parentColumn, entityColumn)` models one-to-many.
99
+ - `associateBy = Junction(...)` models many-to-many through a cross-ref entity.
323
100
 
324
- Always annotate relation queries with `@Transaction` to ensure consistent reads.
101
+ Always annotate relation queries with `@Transaction` for consistent reads.
102
+ Full embedded / one-to-many / junction examples:
103
+ [references/patterns.md#relations](references/patterns.md#relations).
325
104
 
326
105
  ## Migration Strategies
327
106
 
328
- ### Auto Migration (Room 2.4+)
107
+ - Auto migration (`autoMigrations = [AutoMigration(from, to)]`) for additive
108
+ changes; supply an `AutoMigrationSpec` with `@RenameColumn`/`@DeleteColumn`
109
+ for renames or deletes.
110
+ - Manual `Migration(from, to)` with raw `execSQL` for data transformation or
111
+ table merges; register via `.addMigrations(...)`.
112
+ - `.fallbackToDestructiveMigration()` only for development or throwaway data,
113
+ never in release builds.
329
114
 
330
- For simple schema changes (add column, add table, add index):
115
+ Full auto, manual, and destructive examples:
116
+ [references/patterns.md#migration-strategies](references/patterns.md#migration-strategies).
331
117
 
332
- ```kotlin
333
- @Database(
334
- entities = [FlightEntity::class],
335
- version = 3,
336
- autoMigrations = [
337
- AutoMigration(from = 1, to = 2),
338
- AutoMigration(from = 2, to = 3, spec = Migration2To3::class),
339
- ],
340
- )
341
- abstract class AppDatabase : RoomDatabase()
342
-
343
- // Spec needed for column/table renames or deletes
344
- @RenameColumn(tableName = "flights", fromColumnName = "price", toColumnName = "price_amount")
345
- class Migration2To3 : AutoMigrationSpec
346
- ```
118
+ ## Hilt Integration
347
119
 
348
- ### Manual Migration
120
+ Provide the database as a `@Singleton` from a `@Module @InstallIn(SingletonComponent::class)`
121
+ object, and provide each DAO from the database instance. Full `DatabaseModule`:
122
+ [references/patterns.md#hilt-integration](references/patterns.md#hilt-integration).
349
123
 
350
- For complex changes (data transformation, merging tables):
124
+ ## Encrypted Database (SQLCipher)
351
125
 
352
- ```kotlin
353
- val MIGRATION_3_4 = object : Migration(3, 4) {
354
- override fun migrate(db: SupportSQLiteDatabase) {
355
- db.execSQL("""
356
- CREATE TABLE IF NOT EXISTS bookings_new (
357
- id TEXT NOT NULL PRIMARY KEY,
358
- flight_id TEXT NOT NULL,
359
- status TEXT NOT NULL DEFAULT 'PENDING',
360
- created_at INTEGER NOT NULL DEFAULT 0,
361
- FOREIGN KEY (flight_id) REFERENCES flights(id) ON DELETE CASCADE
362
- )
363
- """)
364
- db.execSQL("""
365
- INSERT INTO bookings_new (id, flight_id, status, created_at)
366
- SELECT id, flight_id, status, 0 FROM bookings
367
- """)
368
- db.execSQL("DROP TABLE bookings")
369
- db.execSQL("ALTER TABLE bookings_new RENAME TO bookings")
370
- }
371
- }
126
+ Encrypt the database file by passing a SQLCipher `SupportOpenHelperFactory` to
127
+ `.openHelperFactory(...)` on the builder. This skill owns the encrypted-Room
128
+ wiring; it does not own the passphrase. Derive and store the passphrase through
129
+ the key hierarchy in the `android-security` skill (Keystore-wrapped key), and
130
+ pass the resulting bytes into the factory.
372
131
 
373
- // Register in Hilt module
374
- Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
375
- .addMigrations(MIGRATION_3_4)
376
- .build()
377
- ```
132
+ Full builder wiring:
133
+ [references/patterns.md#encrypted-database-sqlcipher](references/patterns.md#encrypted-database-sqlcipher).
378
134
 
379
- ### Fallback to Destructive Migration
135
+ ## Async Database Provisioning
380
136
 
381
- Only for development or non-critical data:
137
+ When the passphrase (or any setup input) must be derived or fetched before
138
+ `Room.databaseBuilder`, a synchronous `@Provides` cannot await it. Expose the
139
+ database from a suspend, `Mutex`-guarded, double-checked lazy singleton provider
140
+ instead: return the cached instance if present, otherwise build it once inside
141
+ the lock. Hilt provides the provider; repositories fetch DAOs from a suspend
142
+ accessor.
382
143
 
383
- ```kotlin
384
- Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
385
- .fallbackToDestructiveMigration()
386
- .build()
387
- ```
144
+ Full `DatabaseProvider`:
145
+ [references/patterns.md#async-database-provisioning](references/patterns.md#async-database-provisioning).
388
146
 
389
- ## Hilt Integration
147
+ ## Runtime Hygiene
390
148
 
391
- ```kotlin
392
- @Module
393
- @InstallIn(SingletonComponent::class)
394
- object DatabaseModule {
395
-
396
- @Provides
397
- @Singleton
398
- fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
399
- Room.databaseBuilder(
400
- context,
401
- AppDatabase::class.java,
402
- "app.db",
403
- )
404
- .addMigrations(MIGRATION_3_4)
405
- .build()
406
-
407
- @Provides
408
- fun provideFlightDao(database: AppDatabase): FlightDao =
409
- database.flightDao()
410
-
411
- @Provides
412
- fun provideBookingDao(database: AppDatabase): BookingDao =
413
- database.bookingDao()
414
- }
415
- ```
149
+ - Cap WAL file growth on a write-heavy database with an `onOpen` callback issuing
150
+ `PRAGMA wal_autocheckpoint`.
151
+ - Do multi-step read-then-write logic (dedupe, insert, trim-to-cap) atomically in
152
+ a `@Transaction` DAO default method, not across separate DAO calls.
153
+ - `exportSchema = false` is acceptable only for a cache or destructive-fallback
154
+ database whose file is disposable. A migration-tested production database keeps
155
+ `exportSchema = true` and commits its schema JSON, which `MigrationTestHelper`
156
+ validates against.
416
157
 
417
- ## Testing with In-Memory Database
158
+ Full callback and transactional DAO:
159
+ [references/patterns.md#runtime-hygiene](references/patterns.md#runtime-hygiene).
418
160
 
419
- ```kotlin
420
- class FlightDaoTest {
421
-
422
- private lateinit var database: AppDatabase
423
- private lateinit var dao: FlightDao
424
-
425
- @Before
426
- fun setup() {
427
- database = Room.inMemoryDatabaseBuilder(
428
- ApplicationProvider.getApplicationContext(),
429
- AppDatabase::class.java,
430
- )
431
- .allowMainThreadQueries() // OK for tests only
432
- .build()
433
-
434
- dao = database.flightDao()
435
- }
436
-
437
- @After
438
- fun tearDown() {
439
- database.close()
440
- }
441
-
442
- @Test
443
- fun insertAndQuery_returnsCorrectFlight() = runTest {
444
- val flight = FlightEntity(
445
- id = "TK1",
446
- originCode = "IST",
447
- originName = "Istanbul",
448
- destinationCode = "JFK",
449
- destinationName = "New York",
450
- departureTime = "2025-01-15T10:00:00Z",
451
- arrivalTime = "2025-01-15T18:00:00Z",
452
- priceAmount = 799.0,
453
- priceCurrency = "USD",
454
- status = "SCHEDULED",
455
- )
456
-
457
- dao.insertFlight(flight)
458
-
459
- val result = dao.getFlightByIdOnce("TK1")
460
- assertThat(result).isNotNull()
461
- assertThat(result?.originCode).isEqualTo("IST")
462
- }
463
-
464
- @Test
465
- fun flowQuery_emitsUpdates() = runTest {
466
- dao.getFlights("IST", "JFK").test {
467
- assertThat(awaitItem()).isEmpty()
468
-
469
- dao.insertFlight(testFlight)
470
-
471
- val updated = awaitItem()
472
- assertThat(updated).hasSize(1)
473
-
474
- cancelAndIgnoreRemainingEvents()
475
- }
476
- }
477
-
478
- @Test
479
- fun upsert_updatesExistingFlight() = runTest {
480
- dao.insertFlight(testFlight)
481
- val updated = testFlight.copy(status = "DELAYED")
482
- dao.upsertFlights(listOf(updated))
483
-
484
- val result = dao.getFlightByIdOnce(testFlight.id)
485
- assertThat(result?.status).isEqualTo("DELAYED")
486
- }
487
- }
488
- ```
161
+ ## Testing with In-Memory Database
489
162
 
490
- ### Testing Migrations
163
+ Build a `Room.inMemoryDatabaseBuilder(...)` in `@Before`, close it in `@After`,
164
+ and run DAO tests with `runTest`. Use Turbine's `.test { awaitItem() }` for
165
+ `Flow` assertions. Test migrations with `MigrationTestHelper`
166
+ (`createDatabase` at the old version, then `runMigrationsAndValidate`).
491
167
 
492
- ```kotlin
493
- @RunWith(AndroidJUnit4::class)
494
- class MigrationTest {
495
-
496
- @get:Rule
497
- val helper = MigrationTestHelper(
498
- InstrumentationRegistry.getInstrumentation(),
499
- AppDatabase::class.java,
500
- )
501
-
502
- @Test
503
- fun migrate3To4() {
504
- // Create database at version 3
505
- helper.createDatabase("test-db", 3).apply {
506
- execSQL("INSERT INTO bookings (id, flight_id, status) VALUES ('B1', 'F1', 'CONFIRMED')")
507
- close()
508
- }
509
-
510
- // Run migration and validate
511
- val db = helper.runMigrationsAndValidate("test-db", 4, true, MIGRATION_3_4)
512
- val cursor = db.query("SELECT * FROM bookings WHERE id = 'B1'")
513
- assertThat(cursor.moveToFirst()).isTrue()
514
- assertThat(cursor.getColumnIndex("created_at")).isAtLeast(0)
515
- }
516
- }
517
- ```
168
+ Full DAO tests and migration test:
169
+ [references/patterns.md#testing-with-in-memory-database](references/patterns.md#testing-with-in-memory-database).
518
170
 
519
171
  ## Do's and Don'ts
520
172
 
@@ -527,6 +179,10 @@ class MigrationTest {
527
179
  - Use in-memory databases for unit tests
528
180
  - Test migrations with `MigrationTestHelper`
529
181
  - Use `stateIn` with `WhileSubscribed(5000)` in ViewModels for Flow collection
182
+ - Encrypt sensitive databases with a SQLCipher `SupportOpenHelperFactory`
183
+ - Build a database needing async setup from a suspend, `Mutex`-guarded provider
184
+ - Cap WAL growth with an `onOpen` `PRAGMA wal_autocheckpoint`
185
+ - Keep multi-step read-then-write logic inside a `@Transaction` default method
530
186
 
531
187
  ### Don'ts
532
188
  - Do not perform Room operations on the main thread (except in tests with `allowMainThreadQueries`)
@@ -561,4 +217,9 @@ class MigrationTest {
561
217
  - [ ] Migrations tested with `MigrationTestHelper`
562
218
  - [ ] No `fallbackToDestructiveMigration` in release builds
563
219
  - [ ] DAOs provided via Hilt as singletons (database) / transient (DAOs)
220
+ - [ ] Sensitive databases encrypted via a SQLCipher `SupportOpenHelperFactory`
221
+ - [ ] Passphrase sourced from the `android-security` key hierarchy, not reproduced
222
+ - [ ] Async-setup databases built from a suspend, `Mutex`-guarded provider
223
+ - [ ] `onOpen` `PRAGMA wal_autocheckpoint` on write-heavy databases
224
+ - [ ] Multi-step read-then-write logic wrapped in a `@Transaction` default method
564
225
  - [ ] In-memory database used in unit tests