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

Котлин

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

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

классный

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

Котлин

    // 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(...)

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

Поделитесь с нами своими отзывами и идеями через эти ресурсы:

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

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

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

Образцы

Блоги

Кодлабы

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