Irbisa · Шпаргалка

Хранилище Android

Room, DataStore, SharedPreferences, EncryptedSharedPreferences, файлы

Middle-разработчик · 24 вопроса

  1. 01

    Какие способы хранения данных есть в Android и когда использовать каждый?

    Выбор зависит от формы данных: DataStore подходит для небольших настроек, Room для структурированных данных с запросами, приватные файлы для бинарных данных и документов, MediaStore или системный пикер для файлов, видимых пользователю, а Keystore для ключей, которыми шифруется остальное.

  2. 02

    Чем отличаются директории приложения filesDir, cacheDir, noBackupFilesDir и getExternalFilesDir()?

    filesDir, cacheDir и noBackupFilesDir относятся к приватному внутреннему хранилищу; getExternalFilesDir() принадлежит приложению, но лежит на общем томе.

  3. 03

    SharedPreferences: чем отличаются commit() и apply(), и потокобезопасны ли они?

    commit() синхронно пишет на диск в вызывающем потоке и возвращает true или false; apply() сразу обновляет map в памяти, пишет на диск в фоне и ничего не сообщает.

  4. 04

    Что такое Room, из каких частей он состоит и зачем он вместо «голого» SQLite?

    Room представляет собой слой Jetpack поверх SQLite: таблицы описываются через @Entity, запросы через @Dao, база через @Database, а Room генерирует код и проверяет SQL при компиляции.

    @Entity(tableName = "note")data class NoteEntity(  @PrimaryKey(autoGenerate = true) val id: Long = 0,  val title: String,  val createdAt: Long)…
  5. 05

    После add() новая задача появляется, только если уйти с экрана и вернуться. Что не так с этим кодом?

    @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)) }}

    getAll() делает одноразовое чтение: он выполняется один раз в init, и больше никто не спрашивает, поэтому вставка попадает в базу, но не на экран.

  6. 06

    Зачем заменять SharedPreferences на DataStore, и когда выбирать Preferences DataStore, а когда Proto DataStore?

    DataStore исправляет то, что сломано в SharedPreferences: чтение идёт через Flow, запись выполняется атомарными suspend-транзакциями, а ошибки приходят исключениями, а не теряются.

    @Serializabledata class UserSettings(val theme: String = "system", val notifications: Boolean = true) object UserSettingsSerializer : Serializer<UserSettings> {  override val defaultValue = UserSettings()…
  7. 07

    Этот код читает тему из DataStore перед показом UI. Что с ним не так и как сделать правильно?

    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() } }  }}

    runBlocking заставляет главный поток ждать чтения с диска и парсинга во время старта.

  8. 08

    Найдите баг: приложение падает, когда пользователь открывает экран настроек во второй раз.

    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")}

    Каждый SettingsRepository создаёт свой DataStore поверх settings.preferences_pb.

  9. 09

    Почему Room запрещает запросы на главном потоке и на каких потоках выполняются suspend-методы DAO, Flow-запросы и withTransaction?

    Запрос выполняет дисковый I/O на десятки миллисекунд, поэтому для блокирующих вызовов DAO Room бросает IllegalStateException: Cannot access database on the main thread.

  10. 10

    Что такое TypeConverter в Room и какие у них ограничения?

    SQLite хранит только целые числа, вещественные, текст и блобы, поэтому для других типов, например Instant или списка, пишется пара @TypeConverter, которая переводит значение в поддерживаемый тип и обратно, и регистрируется через @TypeConverters.

    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)…
  11. 11

    Как смоделировать связи один-ко-многим и многие-ко-многим в Room и зачем такому запросу @Transaction?

    Форма описывается через @Embedded для родителя и @Relation для детей, а Room выполняет по запросу на каждую сторону и склеивает результаты в памяти.

    @Entity(  tableName = "book",  foreignKeys = [ForeignKey(    entity = AuthorEntity::class, parentColumns = ["id"],    childColumns = ["authorId"], onDelete = ForeignKey.CASCADE  )],…
  12. 12

    Что на самом деле делает OnConflictStrategy.REPLACE и почему @Upsert обычно лучше?

    REPLACE удаляет конфликтующую строку и вставляет новую; это не обновление.

    @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)…
  13. 13

    Как работают миграции Room и что означает падение «Room cannot verify the data integrity»?

    Room хранит в файле базы version и хеш схемы.

    @Database(  entities = [TaskEntity::class],  version = 5,  exportSchema = true,  autoMigrations = [AutoMigration(from = 4, to = 5, spec = AppDatabase.RenameDue::class)])…
  14. 14

    Как протестировать DAO Room и как протестировать миграцию?

    DAO тестируют на настоящей in-memory базе, а не на моке, потому что ценность Room заключается в SQL, который он генерирует.

    @RunWith(AndroidJUnit4::class)class NoteDaoTest {  private val db = Room.inMemoryDatabaseBuilder(    ApplicationProvider.getApplicationContext(), AppDatabase::class.java  ).build()…
  15. 15

    Во время синхронизации Flow списка постоянно переэмитит и экран мерцает, хотя изменилось несколько строк. Почему и как это исправить?

    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>>}

    Каждый upsert вне транзакции выполняется отдельной транзакцией, а инвалидация в Room идёт по таблицам: каждый коммит заставляет всех наблюдателей task заново выполнить весь запрос.

  16. 16

    Найдите баг: при быстрых нажатиях на + количество товара в корзине иногда оказывается меньше числа нажатий.

    @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) }

    addOne() читает количество и пишет current + 1 двумя отдельными запросами, поэтому два нажатия могут оба прочитать 3 и оба записать 4: одно увеличение теряется (lost update).

  17. 17

    Как безопасно хранить токен авторизации на Android и что пришло на смену EncryptedSharedPreferences?

    Сгенерируйте AES-ключ в Android Keystore, зашифруйте им токен и храните только шифротекст в DataStore, файле или Room.

    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 {…
  18. 18

    Что такое scoped storage и как сохранить экспортированный PDF так, чтобы пользователь его нашёл, без WRITE_EXTERNAL_STORAGE?

    Начиная с Android 10 и 11 приложения не могут писать по произвольным путям в общем хранилище; им доступны приватные директории, коллекции MediaStore и системный пикер файлов.

    // 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) }…
  19. 19

    JSON-файл, который пишет приложение, после падения иногда оказывается обрезанным. Почему и как безопасно писать файлы?

    FileOutputStream(target) обрезает файл до записи первого байта, поэтому падение посреди записи оставляет половину файла без старой копии.

    // 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"))…
  20. 20

    Как построить офлайн-кеш на Room и Retrofit, где база служит единственным источником правды?

    UI наблюдает только Room: сеть пишет ответы в Room, а Flow из Room доставляет их на экран, поэтому данные видны сразу, работают офлайн и приходят одним путём.

    class ArticleRepository(  private val api: ArticleApi,  private val dao: ArticleDao,  private val db: AppDatabase) {  // The only thing the UI ever reads…
  21. 21

    Пользователь выходит из аккаунта. Что нужно очистить помимо токена и в каком порядке?

    Удаление токена лишь перестаёт авторизовывать новые запросы; все локальные копии данных предыдущего пользователя остаются на устройстве, и их увидит следующий, кто войдёт.

  22. 22

    Что такое write-ahead logging (WAL) в SQLite и что он меняет для приложения на Room?

    В режиме WAL SQLite дописывает изменения в отдельный файл -wal, а не переписывает страницы базы на месте, поэтому читатели видят последнее закоммиченное состояние, пока писатель пишет.

  23. 23

    Как работают индексы в SQLite и как исправить запрос Room, который замедлился по мере роста таблицы?

    Индекс представляет собой отдельное B-дерево, отсортированное по индексируемым колонкам, поэтому SQLite находит нужные строки за логарифмическое время, а не читает всю таблицу.

    @Entity(  tableName = "message",  indices = [    Index(value = ["chatId", "createdAt"]),      // filter by chat, order by time    Index(value = ["remoteId"], unique = true)  ]…
  24. 24

    Почему данные приложения возвращаются после переустановки и почему восстановленное приложение может падать при расшифровке своих же данных?

    Auto Backup включён по умолчанию: он копирует filesDir, базы данных и SharedPreferences в Google-аккаунт и восстанавливает их при переустановке или на новом телефоне.

    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()…