Data and storage cheat sheet

The 10 Data and storage interview answers in 30 seconds each, plus quick facts. Printable, free.

One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions

Rules of thumb

  • One Room database per process, provided as a singleton.
  • Room Flows re-run on any write to the tables they read.
  • @Transaction for relations and grouped writes.
  • Every version bump needs a tested migration.
  • DataStore instead of SharedPreferences for new code.

Room basics

Room is a layer over SQLite with three parts: @Entity classes for tables, @Dao interfaces with annotated queries, and a @Database class. It checks SQL at compile time against your schema, maps rows to Kotlin objects, supports suspend functions and Flow, and manages migrations. It refuses queries on the main thread unless you opt in, which stops a common source of jank.

How Room Flow queries update

Room tracks which tables each query reads. When any write touches one of those tables, through Room, it re-runs the query and emits the new result. It's table-level, not row-level, so an update to an unrelated row in the same table also triggers a re-run and an emit, which may be equal to the last result.

@Entity(tableName = "notes")
data class NoteRow(@PrimaryKey val id: Long, val text: String)

Relations, transactions and N+1 queries

Model one-to-many relations with a data class that has @Embedded parent and @Relation children; Room loads children in one extra query per relation, not one per parent. Annotate the DAO method with @Transaction so both queries see the same consistent snapshot. Also use @Transaction (or withTransaction) when several writes must succeed or fail together.

@Entity data class Playlist(@PrimaryKey val id: Long, val name: String)
@Entity(indices = [Index("playlistId")])
data class Song(@PrimaryKey val id: Long, val playlistId: Long, val title: String)

data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(parentColumn = "id", entityColumn = "playlistId") val songs: List<Song>,
)

@Dao interface PlaylistDao {
    @Transaction
    @Query("SELECT * FROM Playlist")
    fun observeAll(): Flow<List<PlaylistWithSongs>>
}

Room migrations

Bump the database version and provide a migration from each old version. For simple changes like adding a column or table, @AutoMigration(from = 1, to = 2) generates it from the exported schemas; for renames or deletes add an AutoMigrationSpec. For complex changes write a Migration with SQL. Test every path with MigrationTestHelper against the exported schema files.

@Entity data class Task(
    @PrimaryKey val id: Long,
    val title: String,
    @ColumnInfo(defaultValue = "0") val pinned: Boolean = false,
)

@Database(
    entities = [Task::class],
    version = 4,
    autoMigrations = [AutoMigration(from = 3, to = 4)],
)
abstract class TaskDb : RoomDatabase()

DataStore vs SharedPreferences

SharedPreferences has a synchronous API that can read disk on the main thread, apply() can block the main thread at lifecycle points such as onStop while pending writes flush, and errors are silent. Jetpack DataStore is asynchronous with coroutines and Flow, writes transactionally, reports errors as exceptions, and can store typed objects. Preferences DataStore is for key-value pairs; Proto or typed DataStore is for a schema.

val Context.settings by preferencesDataStore(name = "settings")
private val DARK = booleanPreferencesKey("dark_mode")

class SettingsRepository(private val context: Context) {
    val darkMode: Flow<Boolean> = context.settings.data.map { it[DARK] ?: false }
    suspend fun setDarkMode(on: Boolean) { context.settings.edit { it[DARK] = on } }
}

Scoped storage

Since Android 10, and enforced from Android 11, apps get unrestricted access only to their own app-specific directories. Shared media goes through MediaStore: your app can add photos, video and audio without permission, and reading others' media needs READ_MEDIA_* on Android 13+. For other documents, use the Storage Access Framework, where the user picks files. Broad storage permissions are restricted on Play.

Paging 3

Paging 3 loads data in pages as the user scrolls. A PagingSource loads one page by key; Pager(...).flow exposes Flow<PagingData<T>>, which you cachedIn(viewModelScope) and collect with collectAsLazyPagingItems() in Compose. For offline-first, a RemoteMediator fetches pages from the network into Room, and Room's own PagingSource feeds the UI.

@Dao
interface FeedDao {
    @Query("SELECT * FROM ArticleRow ORDER BY id")
    fun paging(): PagingSource<Int, ArticleRow>
}

@Entity data class ArticleRow(@PrimaryKey val id: Long, val title: String)

class PagedFeedViewModel(dao: FeedDao) : ViewModel() {
    val feed: Flow<PagingData<ArticleRow>> =
        Pager(PagingConfig(pageSize = 30)) { dao.paging() }.flow.cachedIn(viewModelScope)
}

What to clear on logout

Everything tied to that user: tokens in secure storage, the Room database or the user's rows, DataStore preferences that are per-user, in-memory caches and repositories holding state, image and HTTP caches with private content, scheduled WorkManager jobs, notification channels or pending notifications, and push token registration on your server. Then reset navigation to the login screen.

Database performance

Look at the query plan with EXPLAIN QUERY PLAN (Database Inspector in Android Studio can run it) to see whether SQLite scans the whole table. Add indexes on columns used in WHERE, JOIN and ORDER BY. Select only the columns you need, page large results, batch writes in transactions, and keep the database work off the main thread.

@Entity(indices = [Index(value = ["chatId", "sentAt"])])
data class Message(@PrimaryKey val id: Long, val chatId: Long, val sentAt: Long, val body: String)

Cache freshness and stale data

Store when each piece of data was fetched and pick a policy per data type. Show cached data immediately, then refresh in the background if it's older than its time-to-live (stale-while-revalidate). Refresh on pull-to-refresh regardless. For data that must be current, like a balance, show a 'last updated' time and block actions until fresh data arrives.

Quick facts

@Upsert
Room's @Upsert inserts a row or updates it if the primary key exists.
Database Inspector
Android Studio's App Inspection lets you browse and query a running app's Room database.
clearAllTables()
RoomDatabase.clearAllTables() deletes all rows but keeps the schema.
Proto DataStore
Proto DataStore stores a typed object defined with protocol buffers or a custom serializer.