DataStore Часть Android Jetpack .

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 выдаст исключение IllegalStateException при чтении или обновлении данных.

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

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

Настраивать

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

Preferences DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

Сохраняйте пары ключ-значение с помощью Preferences DataStore.

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

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

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

Котлин

// 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));

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

Preferences DataStore предоставляет функцию edit() , которая транзакционно обновляет данные в DataStore . Параметр 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 требуется предопределенная схема в файле прототипа в каталоге app/src/main/proto/ . Эта схема определяет тип объектов, которые вы сохраняете в своем хранилище данных Proto. Дополнительные сведения об определении протосхемы см. в руководстве по языку protobuf .

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Создайте прототип хранилища данных

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

  1. Определите класс, реализующий Serializer<T> , где T — тип, определенный в файле прототипа. Этот класс сериализатора сообщает DataStore, как читать и записывать ваш тип данных. Обязательно укажите значение по умолчанию для сериализатора, который будет использоваться, если файл еще не создан.
  2. Используйте делегат свойства, созданный dataStore , чтобы создать экземпляр DataStore<T> , где T — это тип, определенный в файле прототипа. Вызовите это один раз на верхнем уровне вашего файла 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.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

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. Служба работает в отдельном процессе и периодически обновляет хранилище данных.

    <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): Timer =
       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, посетите следующие дополнительные ресурсы:

Образцы

Блоги

Кодлабы

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