Course outline

Android development / Intermediate: state, data, and inclusive features / Lesson 8

Room, DataStore, and durable data

Choose storage by data shape and consistency needs, then preserve user records through transactions and schema evolution.

About 4 min readContent reviewed
On this page

Lesson goals

  • Separate relational records from small settings.
  • Explain transactions, migrations, and observable queries.

Match storage to the invariant

A reading list has multiple records, stable ids, queries, and possibly relationships, so a relational database is a natural fit. Room adds typed access and query checking over SQLite. DataStore suits small settings or typed configuration updated consistently, such as a preferred sort order. Neither tool automatically synchronizes data across devices. Files suit larger binary content, while the database can retain metadata and ownership. Define what must survive process death, application updates, logout, and backup restoration before choosing a storage location or promising persistence to a user.

Define records and access together

The excerpt uses the androidx.room API family, commonly found in Room 2 projects. Select a compatible pinned library line and matching compiler/KSP configuration; do not mix these imports with a different major API family from newer documentation. The entity specifies stored columns and the DAO specifies allowed operations. An observed query emits when relevant tables change, allowing the UI to follow durable truth. Keep domain-specific validation outside raw SQL mapping and use a repository to decide how database values become screen state.

kotlin Entity and DAO for a compatible Room project
@Entity(tableName = "reading_items")
data class ReadingEntity(
    @PrimaryKey val id: String,
    val title: String,
    val finished: Boolean
)

@Dao
interface ReadingDao {
    @Query("SELECT * FROM reading_items ORDER BY title, id")
    fun observeAll(): Flow<List<ReadingEntity>>

    @Upsert
    suspend fun save(item: ReadingEntity)
}

Import androidx.room annotations and kotlinx.coroutines.flow.Flow. The chosen Room release must support Upsert. Add a RoomDatabase class listing this entity and DAO, configure its generated implementation, and inject the DAO; this excerpt is not a complete database setup.

Protect multi-step changes

A transaction makes a group of database operations succeed or fail together. For example, marking a record as changed and adding a pending synchronization operation must be atomic, otherwise a crash can leave an edit that never uploads. A coroutine does not itself provide database atomicity. Avoid holding transactions open across network calls because remote latency is unbounded and locks affect local readers and writers. Read the required local state, perform remote work outside the transaction, then apply a validated result through a short transaction with an explicit conflict policy.

Keep one settings store per file

Preferences DataStore represents keys and values without a relational schema. Declare its instance once at a suitable scope and expose settings through a repository. The edit transform updates preferences atomically, avoiding a separate unsynchronized read and write. A missing key can map to a documented default; an I/O failure is a different condition and should not silently erase user intent. Do not create multiple active DataStore instances for the same file. If settings require relationships or frequent partial record updates, reconsider whether a database is the better abstraction.

kotlin One top-level Preferences DataStore delegate
val Context.readingSettings by preferencesDataStore(name = "reading_settings")
private val SORT_BY_TITLE = booleanPreferencesKey("sort_by_title")

suspend fun setTitleSort(context: Context, enabled: Boolean) {
    context.readingSettings.edit { values ->
        values[SORT_BY_TITLE] = enabled
    }
}

Use the datastore-preferences dependency, Context, preferencesDataStore, booleanPreferencesKey, and edit imports. Keep this delegate in one file and call through an injected application-scoped settings repository. The suspend function reports completion only after the edit operation completes.

Treat migrations as user-data changes

Changing an entity can require a schema version increment and a migration that preserves existing records. Export schemas and test an actual old database upgraded to the new version, including default values and constraints. Destructive recreation may be acceptable for disposable cache data but is unsuitable as a casual fix for valuable user records. Backups can restore data created by earlier releases, so fresh-install testing alone is insufficient. Verify query ordering explicitly, and provide stable tie breakers when the UI or pagination depends on repeatable order.

Common errors and fixes

A schema change loses saved records

Why it happens
Destructive recreation replaced a required migration.
How to fix it
Restore from a safe test backup and implement a data-preserving migration.
Prevent it next time
Test upgrade paths before distributing schema changes.

Two settings writers overwrite each other

Why it happens
Separate read/write logic or duplicate store instances violate ownership.
How to fix it
Use one store and its atomic update transform.
Prevent it next time
Centralize store construction and test concurrent updates.

UI stalls during database access

Why it happens
A blocking query runs on the main thread.
How to fix it
Use supported suspend or Flow DAO APIs and appropriate dispatching.
Prevent it next time
Keep repositories main-safe and avoid disabling thread safeguards.

Check your understanding

1. Classify preferred theme, ten thousand reading records, and downloaded cover images by storage type.

Show worked solution

A small theme preference belongs in DataStore. Queryable records belong in Room with a migration policy. Images belong in appropriate files or cache storage, with database metadata if necessary. Define whether images are reproducible cache or unique user content because that changes cleanup and backup expectations.

2. Saving a title and enqueuing its upload use two independent writes. Find the crash window and repair the contract.

Show worked solution

A crash after the title write but before queue insertion leaves unsynchronized data with no work record. Store the edit and pending operation in one database transaction. A scheduler can later discover pending operations, so even interruption between database commit and scheduling does not erase the durable intention.

Key takeaways

  • Storage choices follow data shape and recovery requirements.
  • Atomic persistence is separate from coroutine execution.
  • Migration tests protect existing users, not only fresh installs.

Official references