DataStore Część pakietu Android Jetpack.

Jetpack DataStore to rozwiązanie do przechowywania danych, które umożliwia przechowywanie par klucz-wartość lub obiektów wpisanych za pomocą buforów protokołów. DataStore wykorzystuje współprogramy Kotlin i Flow do przechowywania danych asynchronicznie, spójnie i transakcyjnie.

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.

  • Preferencje DataStore przechowuje i uzyskuje dostęp do danych za pomocą kluczy. Ta implementacja nie wymaga wstępnie zdefiniowanego schematu i nie zapewnia bezpieczeństwa typu.
  • Proto DataStore przechowuje dane jako wystąpienia niestandardowego typu danych. Ta implementacja wymaga zdefiniowania schematu za pomocą buforów protokołów, ale zapewnia bezpieczeństwo typu.

Odpowiednie korzystanie z DataStore

Aby móc prawidłowo korzystać z DataStore, pamiętaj o tych regułach:

  1. Nigdy nie twórz więcej niż 1 instancji elementu DataStore dla danego pliku w ramach tego samego procesu. Może to uniemożliwić działanie wszystkich funkcji DataStore. Jeśli dla danego pliku jest aktywnych wiele magazynów danych w tym samym procesie, DataStore zwróci żądanie IllegalStateException podczas odczytywania lub aktualizowania danych.

  2. Ogólny typ magazynu danych musi być stały. Muszanie typu używanego w DataStore unieważnia wszelkie gwarancje udzielane przez DataStore i tworzy potencjalnie poważne, trudne do wychwycenia błędy. Zdecydowanie zalecamy korzystanie z buforów protokołów, które zapewniają gwarancję niezmienności, prosty interfejs API i wydajną serializację.

  3. Nigdy nie łącz przypadki użycia SingleProcessDataStore i MultiProcessDataStore w tym samym pliku. Jeśli chcesz uzyskać dostęp do usługi DataStore w więcej niż jednym procesie, zawsze używaj MultiProcessDataStore.

Skonfiguruj

Aby użyć Jetpack DataStore w swojej aplikacji, dodaj ten kod do pliku Gradle w zależności od implementacji, której chcesz użyć:

Preferencje (DataStore)

Odlotowy

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

Odlotowy

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

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

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

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

Kotlin

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

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

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

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

Przechowuj pary klucz-wartość w usłudze Preferences DataStore

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

Tworzenie magazynu danych preferencji

Aby utworzyć instancję Datastore<Preferences>, użyj delegata usługi utworzonej przez użytkownika preferencesDataStore. Wywołaj je raz na najwyższym poziomie pliku kotlin i uzyskaj do niego dostęp za pośrednictwem tej właściwości w pozostałej części aplikacji. Dzięki temu łatwiej będzie utrzymać urządzenie DataStore jako pojedyncze. Jeśli używasz RxJava, możesz też użyć polecenia RxPreferenceDataStoreBuilder. Obowiązkowy parametr name to nazwa preferencji w DataStore.

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 preferencji

Ustawienia DataStore nie korzysta ze wstępnie zdefiniowanego schematu, więc musisz użyć odpowiedniej funkcji typu klucza, aby zdefiniować klucz dla każdej wartości, którą chcesz przechowywać w instancji DataStore<Preferences>. Aby np. zdefiniować klucz dla wartości całkowitej, użyj intPreferencesKey(). Następnie użyj właściwości DataStore.data, aby udostępnić odpowiednią wartość zapisaną w elemencie 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 magazynie danych preferencji

Preferencje w DataStore udostępnia funkcję edit(), która przeprowadza transakcyjną aktualizację danych w elemencie DataStore. Parametr transform funkcji akceptuje blok kodu, w którym możesz aktualizować wartości w razie potrzeby. Cały kod w bloku przekształcenia jest traktowany jako jedna 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 obiektów typów za pomocą Proto DataStore

Implementacja Proto DataStore korzysta z DataStore i buforów protokołów do utrwalania wpisanych obiektów na dysku.

Zdefiniuj schemat

Proto DataStore wymaga wstępnie zdefiniowanego schematu w pliku protokołu w katalogu app/src/main/proto/. Ten schemat określa typ obiektów, które przechowujesz w 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 magazynu danych Proto do przechowywania obiektów typów składa się z 2 kroków:

  1. Zdefiniuj klasę, która implementuje funkcję Serializer<T>, gdzie T to typ zdefiniowany w pliku proto. Ta klasa serializatora informuje DataStore o tym, jak odczytywać i zapisywać typ danych. Jeśli nie masz jeszcze utworzonego pliku, pamiętaj o podaniu wartości domyślnej serializatora, który ma być używany.
  2. Użyj delegata właściwości utworzonego przez dataStore, aby utworzyć wystąpienie DataStore<T>, gdzie T to typ zdefiniowany w pliku proto. Wywołaj je na najwyższym poziomie pliku kotlin i uzyskaj dostęp do niego za pomocą tej usługi, która przekazuje dostęp do pozostałych części aplikacji. Parametr filename wskazuje, który plik ma być używany 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 narzędzia DataStore.data, aby udostępnić 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 przeprowadza transakcyjną aktualizację zapisanego obiektu. Funkcja updateData() podaje bieżący stan danych jako instancję typu danych i aktualizuje dane transakcyjnie w niepodzielnej operacji odczytu, zapisu i modyfikowania.

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

Używanie DataStore w kodzie synchronicznym

Jedną z głównych zalet DataStore jest asynchroniczny interfejs API, ale zmiana otaczającego kodu na asynchroniczny może nie zawsze być możliwa. Może się tak zdarzyć, jeśli pracujesz z istniejącą bazą kodu, która korzysta z synchronicznego wejścia-wyjścia dysku, lub korzystasz z zależności, która nie zapewnia asynchronicznego interfejsu API.

współprogramy Kotlin udostępniają konstruktor współprogramów runBlocking(), który pomaga wypełnić lukę między kodem synchronicznym i asynchronicznym. Za pomocą runBlocking() możesz synchronicznie odczytywać dane z DataStore. RxJava oferuje metody blokowania w witrynie Flowable. Ten kod blokuje wątek wywołujący, 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 UI może powodować błędy ANR i zacinanie się interfejsu. Możesz rozwiązać te problemy, asynchroniczne wstępne wczytywanie danych 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();

Dzięki temu DataStore asynchronicznie odczytuje dane i zapisuje je w pamięci podręcznej. Późniejsze odczyty synchroniczne z użyciem funkcji runBlocking() mogą być szybsze lub mogą całkowicie uniknąć operacji wejścia-wyjścia dysku po zakończeniu wstępnego odczytu.

Używanie DataStore w kodzie wieloprocesowym

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

  • Odczyty zwracają tylko dane, które zostały zapisane na dysku.
  • Spójność odczytu po zapisie.
  • Zapisy są zserializowane.
  • Zapisy nigdy nie są blokowane przez odczyty.

Rozważmy przykładową aplikację z usługą i działaniem:

  1. Usługa działa w osobnym procesie i okresowo aktualizuje magazyn danych

    <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 swój 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ą 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. Jeśli nie utworzono jeszcze pliku, pamiętaj o podaniu domyślnej wartości serializatora, który ma być używany. Poniżej znajduje się przykład użycia funkcji 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()
       )
   }
}

Możesz użyć wstrzykiwania zależności Hilt, aby upewnić się, że instancja DataStore jest unikalna w ramach każdego procesu:

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

Prześlij opinię

Podziel się z nami swoją opinią i pomysłami, korzystając z tych zasobów:

Śledzenie problemów
Zgłoś problemy, żebyśmy mogli naprawić błędy.

Dodatkowe materiały

Aby dowiedzieć się więcej o Jetpack DataStore, zobacz te dodatkowe materiały:

Próbki

Blogi

Ćwiczenia z programowania