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 używa coroutines i Flow w Kotlinie do asynchronicznego, spójnego i transakcyjnego przechowywania danych.

Jeśli obecnie używasz usługi SharedPreferences do przechowywania danych, rozważ przeniesienie ich do DataStore.

Preferences 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. W ramach tego samego procesu nigdy nie twórz więcej niż 1 instancji funkcji DataStore dla danego pliku. 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. Mutowanie typu używanego w DataStore unieważnia wszelkie gwarancje zapewniane przez DataStore i może powodować poważne, trudne do wykrycia 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 większej liczby procesów, zawsze używaj MultiProcessDataStore.

Konfiguracja

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

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

Przechowywanie par klucz-wartość w Preferences DataStore

Implementacja Preference DataStore korzysta z klas DataStorePreferences, aby zapisywać na dysku proste pary klucz-wartość.

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 pozostać pojedynczym obiektem. Jeśli używasz RxJava, możesz też użyć polecenia RxPreferenceDataStoreBuilder. Wymagany parametr name to nazwa DataStore 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 magazynu danych Preferencje

Ponieważ DataStore preferencji nie używa wstępnie zdefiniowanego schematu, musisz użyć odpowiedniej funkcji typu klucza, aby zdefiniować klucz dla każdej wartości, którą chcesz zapisać 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 ujawnić odpowiednią wartość przechowywaną za pomocą elementu 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));

Zapisywanie w Preferences 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.

Przechowywanie typowanych obiektów za pomocą Proto DataStore

Implementacja Proto DataStore używa DataStore i buforów protokołów do zapisywania typowanych obiektów na dysku.

Definiowanie schematu

Proto DataStore wymaga wstępnie zdefiniowanego schematu w pliku proto w katalogu app/src/main/proto/. Ten schemat definiuje typ obiektów przechowywanych w Twoim Proto DataStore. 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 składa się z 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();

Odczytywanie z Proto DataStore

Użyj DataStore.data, aby ujawnić Flow odpowiedniej właściwości z zapisanego 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());

Zapisywanie 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.

    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 określa sposób odczytywania i zapisywania typu danych w usłudze DataStore. Pamiętaj, aby podać wartość domyślną dla serializatora, która zostanie użyta, jeśli nie ma jeszcze utworzonego 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 plik na dysku, który jest używany przez 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 udostępnia interfejs API do obsługi uszkodzeń, który może pomóc w łatwym odzyskaniu danych w takim przypadku i uniknięciu 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ć tego modułu obsługi, podaj wartość corruptionHandler podczas tworzenia instancji DataStore w funkcji 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:

Issue Tracker
Zgłaszaj problemy, abyśmy mogli naprawiać błędy.

Dodatkowe materiały

Aby dowiedzieć się więcej o Jetpack DataStore, zapoznaj się z tymi materiałami:

Próbki

Blogi

Ćwiczenia z programowania