Irbisa · Cheatsheet
Android Persistence
Room, DataStore, SharedPreferences, EncryptedSharedPreferences, file storage
What ways are there to store data on Android, and when do you use each?
What is the difference between
filesDir,cacheDir,noBackupFilesDirandgetExternalFilesDir()?SharedPreferences: what is the difference between
commit()andapply(), and is it thread-safe?What is Room, what are its main parts, and why use it instead of raw SQLite?
@Entity(tableName = "note")data class NoteEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val createdAt: Long)…After
add()the new task appears only when the user leaves the screen and comes back. What is wrong with this code?@Daointerface TaskDao { @Query("SELECT * FROM task ORDER BY dueAt") suspend fun getAll(): List<TaskEntity> @Insert suspend fun insert(task: TaskEntity)} class TaskViewModel(private val dao: TaskDao) : ViewModel() { val tasks = MutableStateFlow(emptyList<TaskEntity>()) init { viewModelScope.launch { tasks.value = dao.getAll() } } fun add(title: String) = viewModelScope.launch { dao.insert(TaskEntity(title = title)) }}Why replace SharedPreferences with DataStore, and when do you choose Preferences DataStore over Proto DataStore?
@Serializabledata class UserSettings(val theme: String = "system", val notifications: Boolean = true) object UserSettingsSerializer : Serializer<UserSettings> { override val defaultValue = UserSettings()…This code reads the theme from DataStore before showing the UI. What is wrong with it, and how should it be done?
val Context.settings by preferencesDataStore(name = "settings")val THEME = stringPreferencesKey("theme") class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val theme = runBlocking { settings.data.first()[THEME] } ?: "system" setContent { AppTheme(theme) { App() } } }}Find the bug: the app crashes the second time the user opens the Settings screen.
class SettingsRepository(context: Context) { private val dataStore = PreferenceDataStoreFactory.create { context.preferencesDataStoreFile("settings") } val theme: Flow<String> = dataStore.data.map { it[THEME] ?: "system" } suspend fun setTheme(value: String) { dataStore.edit { it[THEME] = value } }} class SettingsViewModel(app: Application) : AndroidViewModel(app) { private val repo = SettingsRepository(app) val theme = repo.theme.stateIn(viewModelScope, SharingStarted.Eagerly, "system")}Why does Room refuse queries on the main thread, and on which threads do
suspendDAO methods,Flowqueries andwithTransactionrun?What are Room
TypeConverters, and what are their limitations?class Converters { @TypeConverter fun fromInstant(value: Instant?): Long? = value?.toEpochMilli() @TypeConverter fun toInstant(value: Long?): Instant? = value?.let(Instant::ofEpochMilli) // Fine to read back, impossible to query on @TypeConverter fun fromTags(tags: List<String>): String = Json.encodeToString(tags)…How do you model one-to-many and many-to-many relations in Room, and why does such a query need
@Transaction?@Entity( tableName = "book", foreignKeys = [ForeignKey( entity = AuthorEntity::class, parentColumns = ["id"], childColumns = ["authorId"], onDelete = ForeignKey.CASCADE )],…What does
OnConflictStrategy.REPLACEactually do, and why is@Upsertusually the better choice?@Entity(tableName = "contact")data class ContactEntity( @PrimaryKey val remoteId: String, // the server id is the key, so @Upsert can match it val name: String, val isFavourite: Boolean = false // local only)…How do Room migrations work, and what does the crash "Room cannot verify the data integrity" mean?
@Database( entities = [TaskEntity::class], version = 5, exportSchema = true, autoMigrations = [AutoMigration(from = 4, to = 5, spec = AppDatabase.RenameDue::class)])…How do you test a Room DAO, and how do you test a migration?
@RunWith(AndroidJUnit4::class)class NoteDaoTest { private val db = Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), AppDatabase::class.java ).build()…During a sync the list
Flowkeeps re-emitting and the screen flickers, though only a few rows changed. Why, and how do you fix it?suspend fun applyRemote(rows: List<TaskEntity>) { rows.forEach { dao.upsert(it) }} @Daointerface TaskDao { @Upsert suspend fun upsert(task: TaskEntity) @Query("SELECT * FROM task ORDER BY dueAt") fun observeAll(): Flow<List<TaskEntity>>}Find the bug: when the user taps
+quickly, the cart quantity sometimes ends up lower than the number of taps.@Daointerface CartDao { @Query("SELECT * FROM cart_item WHERE productId = :id") suspend fun find(id: Long): CartItemEntity? @Upsert suspend fun upsert(item: CartItemEntity)} class CartRepository(private val dao: CartDao) { suspend fun addOne(productId: Long) { val current = dao.find(productId)?.quantity ?: 0 dao.upsert(CartItemEntity(productId, quantity = current + 1)) }}// CartViewModel: fun onPlusClick(id: Long) = viewModelScope.launch { repo.addOne(id) }How do you store an auth token securely on Android, and what replaced
EncryptedSharedPreferences?private const val ALIAS = "auth" private fun key(): SecretKey { val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } (ks.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry)?.let { return it.secretKey } return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").run {…What is scoped storage, and how do you save an exported PDF where the user can find it without
WRITE_EXTERNAL_STORAGE?// Option 1: the user picks the destination, no permission at allval createPdf = registerForActivityResult( ActivityResultContracts.CreateDocument("application/pdf")) { uri -> if (uri != null) lifecycleScope.launch(Dispatchers.IO) { contentResolver.openOutputStream(uri)?.use { exporter.writeTo(it) }…A JSON file the app writes sometimes comes back truncated after a crash. Why, and how do you write files safely?
// wrong: opening the target truncates it firstFile(context.filesDir, "cart.json").writeText(json) // right: backup + sync, the last good bytes survive a failed writeprivate val cartFile = AtomicFile(File(context.filesDir, "cart.json"))…How do you build an offline cache with Room and Retrofit where the database is the single source of truth?
class ArticleRepository( private val api: ArticleApi, private val dao: ArticleDao, private val db: AppDatabase) { // The only thing the UI ever reads…The user logs out. What must you clear besides the token, and in what order?
What is write-ahead logging (WAL) in SQLite, and what does it change for a Room app?
How do indexes work in SQLite, and how would you fix a Room query that slowed down as the table grew?
@Entity( tableName = "message", indices = [ Index(value = ["chatId", "createdAt"]), // filter by chat, order by time Index(value = ["remoteId"], unique = true) ]…Why does app data come back after a reinstall, and why can a restored app crash while decrypting its own data?
private val sealed = File(context.noBackupFilesDir, "session.bin") // never backed up fun loadRefreshToken(): String? = try { if (!sealed.exists()) null else crypto.decrypt(sealed.readBytes()).decodeToString()} catch (e: GeneralSecurityException) { // key missing: data restored from another device sealed.delete()…