DataStore Часть Android Jetpack .

Попробуйте Kotlin Multiplatform
Kotlin Multiplatform позволяет использовать уровень данных совместно с другими платформами. Узнайте, как настроить и использовать DataStore в KMP.

Jetpack DataStore — это решение для хранения данных, позволяющее хранить пары «ключ-значение» или типизированные объекты с помощью буферов протоколов . DataStore использует сопрограммы Kotlin и Flow для асинхронного, согласованного и транзакционного хранения данных.

Если вы в настоящее время используете SharedPreferences для хранения данных, рассмотрите возможность перехода на DataStore.

Настройки DataStore и Proto DataStore

DataStore предоставляет две различные реализации: Preferences DataStore и Proto DataStore.

  • Preferences DataStore хранит данные и обеспечивает доступ к ним с помощью ключей. Эта реализация не требует предопределённой схемы и не обеспечивает типобезопасность.
  • Proto DataStore хранит данные как экземпляры пользовательского типа данных. Эта реализация требует определения схемы с использованием буферов протокола , но обеспечивает типобезопасность.

Правильное использование DataStore

Для правильного использования DataStore всегда помните о следующих правилах:

  1. Никогда не создавайте более одного экземпляра DataStore для одного файла в одном процессе. Это может привести к нарушению работы DataStore. Если для одного файла в одном процессе активны несколько экземпляров DataStore, DataStore выдаст исключение IllegalStateException при чтении или обновлении данных.

  2. Обобщённый тип DataStore<T> должен быть неизменяемым. Изменение типа, используемого в DataStore, аннулирует все гарантии, предоставляемые DataStore, и создаёт потенциально серьёзные, труднообнаружимые ошибки. Настоятельно рекомендуется использовать буферы протоколов, которые обеспечивают гарантии неизменяемости, простой API и эффективную сериализацию.

  3. Никогда не смешивайте использование SingleProcessDataStore и MultiProcessDataStore для одного и того же файла. Если вы планируете обращаться к DataStore из нескольких процессов, всегда используйте MultiProcessDataStore .

Настраивать

Чтобы использовать Jetpack DataStore в своем приложении, добавьте в файл Gradle следующее в зависимости от того, какую реализацию вы хотите использовать:

Настройки хранилища данных

классный

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.1.7"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.7"
    }
    

Котлин

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.7")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.7")
    }
    

Прото хранилище данных

классный

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.7"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.7"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.7"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.7"
    }
    

Котлин

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.7")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.7")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.7")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.7")
    }
    

Храните пары «ключ-значение» с помощью Preferences DataStore

Реализация Preferences DataStore использует классы DataStore и Preferences для сохранения простых пар «ключ-значение» на диске.

Создать хранилище данных настроек

Используйте делегат свойства, созданный preferencesDataStore , для создания экземпляра DataStore<Preferences> . Вызовите его один раз на верхнем уровне файла Kotlin и обращайтесь к нему через это свойство во всем остальном приложении. Это упрощает сохранение DataStore как синглтона. В качестве альтернативы, если вы используете RxJava, используйте RxPreferenceDataStoreBuilder . Обязательный параметр name — это имя хранилища данных Preferences.

Котлин

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Ява

RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

Чтение из хранилища данных настроек

Поскольку Preferences DataStore не использует предопределённую схему, необходимо использовать соответствующую функцию типа ключа для определения ключа для каждого значения, которое необходимо сохранить в экземпляре DataStore<Preferences> . Например, чтобы определить ключ для значения типа int, используйте intPreferencesKey() . Затем используйте свойство DataStore.data для предоставления соответствующего сохранённого значения с помощью Flow .

Котлин

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

Ява

Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

Запись в хранилище данных настроек

Настройки DataStore предоставляют функцию edit() , которая транзакционно обновляет данные в DataStore . Параметр transform этой функции принимает блок кода, в котором можно обновлять значения по мере необходимости. Весь код в блоке transform рассматривается как одна транзакция.

Котлин

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

Ява

Single<Preferences> updateResult =  dataStore.updateDataAsync(prefsIn -> {
  MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
  Integer currentInt = prefsIn.get(INTEGER_KEY);
  mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
  return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

Храните типизированные объекты с помощью Proto DataStore

Реализация Proto DataStore использует DataStore и буферы протокола для сохранения типизированных объектов на диске.

Определить схему

Для Proto DataStore требуется предопределенная схема в proto-файле в каталоге app/src/main/proto/ . Эта схема определяет тип объектов, сохраняемых в Proto DataStore. Подробнее об определении proto-схемы см. в руководстве по языку protobuf .

syntax = "proto3";

option java_package = "com.example.application.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Создать Proto DataStore

Создание Proto DataStore для хранения типизированных объектов состоит из двух этапов:

  1. Определите класс, реализующий Serializer<T> , где T — тип, определённый в proto-файле. Этот класс сериализатора сообщает DataStore, как читать и записывать ваш тип данных. Убедитесь, что вы указали значение по умолчанию для сериализатора, которое будет использоваться, если файл ещё не создан.
  2. Используйте делегат свойства, созданный dataStore , для создания экземпляра DataStore<T> , где T — тип, определённый в proto-файле. Вызовите его один раз на верхнем уровне файла Kotlin и обращайтесь к нему через этот делегат свойства во всём остальном приложении. Параметр filename указывает DataStore, какой файл использовать для хранения данных, а параметр serializer сообщает DataStore имя класса сериализатора, определённого на шаге 1.

Котлин

object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  override suspend fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

  override suspend fun writeTo(
    t: Settings,
    output: OutputStream) = t.writeTo(output)
}

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

Ява

private static class SettingsSerializer implements Serializer<Settings> {
  @Override
  public Settings getDefaultValue() {
    Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException(Cannot read proto., exception);
    }
  }

  @Override
  public void writeTo(Settings t, @NotNull OutputStream output) {
    t.writeTo(output);
  }
}

RxDataStore<Byte> dataStore =
    new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();

Чтение из Proto DataStore

Используйте DataStore.data для отображения Flow соответствующего свойства из вашего сохраненного объекта.

Котлин

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

Ява

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

Запись в Proto DataStore

Proto DataStore предоставляет функцию updateData() , которая транзакционно обновляет сохраненный объект. updateData() возвращает текущее состояние данных как экземпляр вашего типа данных и обновляет данные транзакционно в атомарной операции чтения-записи-изменения.

Котлин

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

Ява

Single<Settings> updateResult =
  dataStore.updateDataAsync(currentSettings ->
    Single.just(
      currentSettings.toBuilder()
        .setExampleCounter(currentSettings.getExampleCounter() + 1)
        .build()));

Использование DataStore в синхронном коде

Одним из основных преимуществ DataStore является асинхронный API, но не всегда возможно сделать окружающий код асинхронным. Это может быть актуально, если вы работаете с существующей кодовой базой, использующей синхронный дисковый ввод-вывод, или если у вас есть зависимость, не предоставляющая асинхронный API.

Корутины Kotlin предоставляют конструктор корутин runBlocking() , помогающий преодолеть разрыв между синхронным и асинхронным кодом. Вы можете использовать runBlocking() для синхронного чтения данных из DataStore. RxJava предлагает блокирующие методы для Flowable . Следующий код блокирует вызывающий поток до тех пор, пока DataStore не вернет данные:

Котлин

val exampleData = runBlocking { context.dataStore.data.first() }

Ява

Settings settings = dataStore.data().blockingFirst();

Выполнение синхронных операций ввода-вывода в потоке пользовательского интерфейса может привести к ошибкам ANR или подтормаживаниям пользовательского интерфейса. Эти проблемы можно устранить, выполнив асинхронную предварительную загрузку данных из DataStore:

Котлин

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Ява

dataStore.data().first().subscribe();

Таким образом, DataStore асинхронно считывает данные и кэширует их в памяти. Последующие синхронные чтения с использованием runBlocking() могут выполняться быстрее или вообще исключать операцию дискового ввода-вывода, если первоначальное чтение уже завершено.

Используйте DataStore в многопроцессном коде

Вы можете настроить DataStore для доступа к одним и тем же данным из разных процессов с теми же гарантиями согласованности данных, что и в рамках одного процесса. В частности, DataStore гарантирует:

  • Чтение возвращает только те данные, которые были сохранены на диске.
  • Согласованность чтения после записи.
  • Записи сериализуются.
  • Чтение никогда не блокируется записью.

Рассмотрим пример заявки с услугой и действием:

  1. Служба работает в отдельном процессе и периодически обновляет DataStore.

    <service
      android:name=".MyService"
      android:process=":my_process_id" />
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
          scope.launch {
              while(isActive) {
                  dataStore.updateData {
                      Settings(lastUpdate = System.currentTimeMillis())
                  }
                  delay(1000)
              }
          }
    }
    
  2. В то время как приложение будет собирать эти изменения и обновлять свой пользовательский интерфейс

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

Чтобы иметь возможность использовать DataStore в различных процессах, необходимо создать объект DataStore с помощью MultiProcessDataStoreFactory .

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)

serializer сообщает DataStore, как читать и записывать ваш тип данных. Убедитесь, что вы указали значение по умолчанию для сериализатора, которое будет использоваться, если файл ещё не создан. Ниже приведён пример реализации с использованием kotlinx.serialization :

@Serializable
data class Settings(
   val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   override val defaultValue = Settings(lastUpdate = 0)

   override suspend fun readFrom(input: InputStream): Settings =
       try {
           Json.decodeFromString(
               Settings.serializer(), input.readBytes().decodeToString()
           )
       } catch (serialization: SerializationException) {
           throw CorruptionException("Unable to read Settings", serialization)
       }

   override suspend fun writeTo(t: Settings, output: OutputStream) {
       output.write(
           Json.encodeToString(Settings.serializer(), t)
               .encodeToByteArray()
       )
   }
}

Вы можете использовать внедрение зависимостей Hilt , чтобы гарантировать уникальность вашего экземпляра DataStore для каждого процесса:

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

Обработка повреждения файлов

В редких случаях постоянный файл DataStore на диске может быть повреждён. По умолчанию DataStore не восстанавливается автоматически после повреждения, и попытки чтения из него приведут к возникновению исключения CorruptionException .

DataStore предлагает API-интерфейс обработчика повреждений, который поможет вам корректно восстановить данные в подобной ситуации, избежав возникновения исключения. После соответствующей настройки обработчик повреждений заменяет повреждённый файл новым, содержащим предопределённое значение по умолчанию.

Чтобы настроить этот обработчик, укажите corruptionHandler при создании экземпляра DataStore в by dataStore() или в фабричном методе DataStoreFactory :

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

Оставьте отзыв

Поделитесь с нами своими отзывами и идеями с помощью следующих ресурсов:

Отслеживание проблем
Сообщайте о проблемах, чтобы мы могли исправить ошибки.

Дополнительные ресурсы

Чтобы узнать больше о Jetpack DataStore, ознакомьтесь со следующими дополнительными ресурсами:

Образцы

Блоги

Codelabs

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}