DataStore   Należy do Android Jetpack.

Jetpack DataStore to rozwiązanie do przechowywania danych, które umożliwia przechowywanie par klucz-wartość lub typowanych obiektów za pomocą buforów protokołu. DataStore wykorzystuje współrzędne i flow Kotlin do asynchronicznego, spójnego i transakcyjnego przechowywania danych.

Jeśli do przechowywania danych używasz obecnie SharedPreferences, rozważ migrację do DataStore.

Preferencje DataStore i Proto DataStore

DataStore udostępnia 2 różne implementacje: Preferences DataStore i Proto DataStore.

  • Magazyn danych preferencji przechowuje dane i dostępuje do nich za pomocą kluczy. Ta implementacja nie wymaga wstępnie zdefiniowanego schematu i nie zapewnia bezpieczeństwa typów.
  • Proto DataStore przechowuje dane jako instancje niestandardowego typu danych. Ta implementacja wymaga zdefiniowania schematu za pomocą buforów protokołu, ale zapewnia bezpieczeństwo typów.

Prawidłowe korzystanie z magazynu danych

Aby prawidłowo korzystać z DataStore, pamiętaj o tych zasadach:

  1. Nigdy nie twórz więcej niż 1 instancji DataStore dla danego pliku w ramach tego samego procesu. Może to spowodować przerwanie działania wszystkich funkcji DataStore. Jeśli w ramach tego samego procesu w przypadku danego pliku jest aktywnych kilka obiektów DataStore, podczas odczytu lub aktualizowania danych obiekt DataStore IllegalStateException.

  2. Typ ogólny DataStore musi być niezmienny. Zmiana typu używanego w DataStore unieważnia wszelkie gwarancji, które DataStore tworzy i tworzy potencjalnie poważne, trudne do przechwycenia błędy. Zdecydowanie zalecamy używanie interfejsu protocol buffers, który zapewnia trwałość, prosty interfejs API i skuteczną serializację.

  3. Nigdy nie mieszaj atrybutów SingleProcessDataStore i MultiProcessDataStore w tym samym pliku. Jeśli chcesz uzyskać dostęp do DataStore z kilku procesów, zawsze używaj MultiProcessDataStore.

Konfiguracja

Aby używać Jetpack DataStore w aplikacji, dodaj do pliku Gradle odpowiedniej implementacji:

Preferencje DataStore

Odlotowe

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

Magazyn danych Proto

Odlotowe

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

Przechowywanie par klucz-wartość w Preferences DataStore

Implementacja preferencji DataStore używa klas DataStore i Preferences do zachowania prostych par klucz-wartość na dysku.

Tworzenie magazynu danych ustawień

Aby utworzyć instancję DataStore<Preferences>, użyj obiektu zastępczego usługi utworzonego przez preferencesDataStore. Wywołaj go raz na najwyższym poziomie pliku Kotlin i używaj go za pomocą tej właściwości w pozostałych częściach aplikacji. Dzięki temu DataStore będzie łatwiej zachować jako pojedynczy obiekt. Możesz też użyć RxPreferenceDataStoreBuilder, jeśli używasz RxJava. Obowiązkowy parametr name to nazwa magazynu danych preferencji.

Kotlin

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

Java

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

Odczyt z preferencji DataStore

Preferencje DataStore nie używają wstępnie zdefiniowanego schematu, dlatego musisz użyć odpowiedniej funkcji typu klucza, aby zdefiniować klucz dla każdej wartości, którą chcesz przechowywać w instancji DataStore<Preferences>. Aby na przykład zdefiniować klucz dla wartości typu int, użyj intPreferencesKey(). Następnie użyj właściwości DataStore.data, aby udostępnić odpowiednią wartość zapisaną za pomocą Flow.

Kotlin

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

Java

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

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

Zapisz w preferencji DataStore

Preferences DataStore udostępnia funkcję edit(), która aktualizuje dane w ramach transakcji DataStore. Parametr transform funkcji może zawierać blok kodu, w którym możesz w razie potrzeby aktualizować wartości. Cały kod w bloku transformacji jest traktowany jako pojedyncza transakcja.

Kotlin

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

Java

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.

Przechowuj obiekty wpisywane za pomocą Proto DataStore

Implementacja Proto DataStore korzysta z DataStore i buforów protokołu, aby zapisywać typowane obiekty na dysku.

Definiowanie schematu

Proto DataStore wymaga wstępnie zdefiniowanego schematu w pliku proto w katalogu app/src/main/proto/. Ten schemat określa typ obiektów, które zostaną zachowane w magazynie danych Proto. Więcej informacji o definiowaniu schematu proto znajdziesz w przewodniku po języku protobuf.

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Tworzenie Proto DataStore

Tworzenie schowu danych Proto na potrzeby przechowywania typowanych obiektów wymaga wykonania 2 etapów:

  1. Zdefiniuj klasę, która implementuje Serializer<T>, gdzie T to typ zdefiniowany w pliku proto. Ta klasa serializacji informuje DataStore, jak odczytywać i zapisywać Twój typ danych. Pamiętaj, aby podać wartość domyślną dla serializatora, która będzie używana, jeśli nie ma jeszcze utworzonego pliku.
  2. Użyj obiektu zastępczego właściwości utworzonego przez dataStore, aby utworzyć instancję DataStore<T>, gdzie T to typ zdefiniowany w pliku proto. Zadzwoń do niego raz na najwyższym poziomie pliku kotlin i uzyskaj do niego dostęp za pomocą tego delegowanego obiektu w pozostałych częściach aplikacji. Parametr filename informuje DataStore, którego pliku należy użyć do przechowywania danych, a parametr serializer informuje DataStore o nazwie klasy serializatora zdefiniowanej w kroku 1.

Kotlin

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
)

Java

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

Odczyt z Proto DataStore

Użyj funkcji DataStore.data, aby ujawnić Flow odpowiedniej właściwości z przechowywanego obiektu.

Kotlin

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

Java

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

Zapisz w Proto DataStore

Proto DataStore udostępnia funkcję updateData(), która aktualizuje przechowywany obiekt w ramach transakcji. updateData() zwraca bieżący stan danych jako instancję typu danych i zmienia dane w ramach transakcji w ramach operacji odczytu, zapisu i modyfikacji.

Kotlin

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

Java

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

Korzystanie z DataStore w kodzie synchronicznym

Jedną z głównych zalet DataStore jest interfejs API asynchroniczny, ale nie zawsze można zmienić kod otaczający na asynchroniczny. Może się tak zdarzyć, jeśli pracujesz z dotychczasową bazą kodu, która używa asynchronicznego wejścia/wyjścia z dysku, lub jeśli masz zależność, która nie udostępnia asynchronicznego interfejsu API.

Kotlinowe coroutines zapewniają runBlocking()kreator coroutines, który pomaga wypełnić lukę między kodem synchronicznym a asynchronicznym. Za pomocą funkcji runBlocking() możesz odczytywać dane z DataStore w sposób synchroniczny. RxJava udostępnia metody blokowania na Flowable. Poniższy kod blokuje wywołujący wątek, dopóki DataStore nie zwróci danych:

Kotlin

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

Java

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

Wykonywanie synchronicznych operacji wejścia-wyjścia w wątku interfejsu użytkownika może powodować błędy ANR lub problemy z płynnością interfejsu. Możesz ograniczyć te problemy, asynchronicznie wczytując dane z DataStore:

Kotlin

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

Java

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

W ten sposób DataStore asynchronicznie odczytuje dane i zapisze je w pamięci podręcznej. Późniejsze odczyty asynchroniczne za pomocą funkcji runBlocking() mogą być szybsze lub mogą całkowicie uniknąć operacji wejścia/wyjścia z dysku, jeśli początkowe odczytanie zostało już wykonane.

Korzystanie z DataStore w kodzie wieloprocesowym

Możesz skonfigurować DataStore tak, aby uzyskiwał dostęp do tych samych danych w różnych procesach z takimi samymi gwarancjami spójności danych jak w ramach jednego procesu. W szczególności DataStore gwarantuje:

  • Czytanie zwraca tylko dane, które zostały zapisane na dysku.
  • Spójność odczytu po zapisie.
  • Zapisy są serializowane.
  • Czytania nigdy nie są blokowane przez zapisy.

Rozważ przykładową aplikację z usługą i aktywnością:

  1. Usługa działa w ramach osobnego procesu i okresowo aktualizuje 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. Aplikacja zbiera te zmiany i aktualizuje interfejs użytkownika

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

Aby móc używać Datastore w różnych procesach, musisz utworzyć obiekt Datastore za pomocą funkcji MultiProcessDataStoreFactory.

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

serializer informuje DataStore, jak odczytywać i zapisywać typ danych. Pamiętaj, aby podać wartość domyślną serializatora, która będzie używana, gdy nie utworzono jeszcze żadnego pliku. Oto przykładowa implementacja korzystająca z biblioteki 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()
       )
   }
}

Aby mieć pewność, że instancja DataStore jest unikalna dla każdego procesu, możesz użyć zależności Hilt:

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

Rozwiązywanie problemów z uszkodzeniem plików

W rzadkich przypadkach stały plik na dysku DataStore może zostać uszkodzony. Domyślnie DataStore nie przywraca automatycznie danych po uszkodzeniu, a próby ich odczytu spowodują, że system zgłosi błąd CorruptionException.

DataStore oferuje interfejs API do obsługi uszkodzeń, który ułatwia sprawne przywracanie działania w takich sytuacjach bez zgłaszania wyjątku. Po skonfigurowaniu moduł obsługi uszkodzeń zastępuje uszkodzony plik nowym, zawierającym zdefiniowaną wstępnie wartość domyślną.

Aby skonfigurować ten moduł obsługi, podaj corruptionHandler podczas tworzenia instancji DataStore w by dataStore() lub w metodzie fabrycznej DataStoreFactory:

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

Prześlij opinię

Udostępniaj nam swoje opinie i propozycje, korzystając z tych narzędzi:

Narzędzie do śledzenia błędów
Zgłaszaj problemy, abyśmy mogli naprawiać błędy.

Dodatkowe materiały

Więcej informacji o Jetpack DataStore znajdziesz w tych materiałach:

Próbki

Blogi

Ćwiczenia z programowania