Magazyn danych Zawiera Android Jetpack.

Jetpack DataStore to rozwiązanie do przechowywania danych, które umożliwia przechowywanie par klucz-wartość. pary lub wpisane obiekty z protokołem bufory. DataStore używa platformy Kotlin i przepływu do asynchronicznego, spójnego przechowywania danych transakcji.

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

Preferencje DataStore i Proto DataStore

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

  • Preferences DataStore przechowuje dane i uzyskuje do nich dostęp za pomocą kluczy. Ten nie wymaga wstępnie zdefiniowanego schematu i nie zapewnia bezpieczeństwa pisania.
  • Proto DataStore przechowuje dane jako instancje danych niestandardowego typu. Ten wdrożenie wymaga zdefiniowania schematu za pomocą protokołu buforuje, ale podaje typ bezpieczeństwa.

Prawidłowe korzystanie z DataStore

Aby poprawnie korzystać z DataStore, pamiętaj o tych regułach:

  1. Nigdy nie twórz więcej niż jednego wystąpienia ciągu DataStore dla danego pliku w ten sam proces. Może to zakłócić działanie wszystkich funkcji DataStore. Jeśli wiele magazynów DataStore aktywnych dla danego pliku w tym samym procesie, DataStore zgłasza funkcję IllegalStateException podczas odczytywania lub aktualizowania danych.

  2. Ogólny typ magazynu DataStore musi być stały. Mutacja typu używane w DataStore unieważniają wszelkie gwarancje udzielane i tworzone przez DataStore potencjalnie poważnych i trudnych do naprawienia błędy. Zdecydowanie zalecamy użycie bufory protokołów, które zapewniają gwarancje niezmienności, prosty interfejs API wydajną serializację.

  3. Nigdy nie mieszaj użycia funkcji SingleProcessDataStore i MultiProcessDataStore w tym samym pliku. Jeśli chcesz uzyskać dostęp do usługi DataStore z więcej niż jednego zawsze używaj MultiProcessDataStore.

Konfiguracja

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

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 korzysta z metody DataStore i Preferences zachowywanie prostych par klucz-wartość na dysku.

Tworzenie magazynu danych preferencji

Użyj przedstawiciela usługi utworzonego przez użytkownika preferencesDataStore, aby utworzyć instancję Datastore<Preferences>. Wywołuj je raz na najwyższym poziomie pliku kotlin. Uzyskuj do niego dostęp przez tę usługę przez pozostałą część aplikacji. Dzięki temu łatwiej Ci będzie zadbać o to, żeby DataStore był Tobą w pojedynkę. Możesz też użyć narzędzia RxPreferenceDataStoreBuilder w przypadku używania biblioteki RxJava. Obowiązkowy parametr name to nazwa Ustawienia 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 preferencji DataStore

Preferencje DataStore nie korzystają ze wstępnie zdefiniowanego schematu, więc musisz użyć metody odpowiednią funkcję typu klucza, aby zdefiniować klucz dla każdej wartości, którą musisz w instancji DataStore<Preferences>. Aby na przykład zdefiniować klucz dla wartości całkowitej, użyj funkcji intPreferencesKey() Następnie skorzystaj z Usługa DataStore.data aby udostępnić odpowiednią wartość przedpłaconą za pomocą funkcji 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

Preferencje DataStore zapewnia edit() która transakcyjna aktualizuje dane w formacie DataStore. Parametr funkcji Parametr transform akceptuje blok kodu, w którym możesz aktualizować wartości jako niezbędną. Cały kod w bloku przekształcenia jest traktowany jako pojedynczy kod transakcji.

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 protokołu bufory, aby były zapisywane obiektów na dysk.

Zdefiniuj schemat

Proto DataStore wymaga wstępnie zdefiniowanego schematu w pliku proto w Katalog app/src/main/proto/. Ten schemat określa typ obiektów które pozostają w magazynie danych Proto. Aby dowiedzieć się więcej o definiowaniu protokołu schema.org, patrz język protokołu Protobuf .

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Utwórz magazyn danych Proto

Tworzenie bazy danych Proto do przechowywania wpisywanych danych wymaga wykonania 2 etapów obiekty:

  1. Zdefiniuj klasę, która stosuje metodę Serializer<T>, gdzie T jest typem zdefiniowanym w pliku proto. Ta klasa serializowania informuje DataStore, jak odczytywać i zapisywać typ danych. Pamiętaj, aby podać wartość domyślną dla serializatora jest używana, jeśli nie utworzono jeszcze żadnego pliku.
  2. Użyj przedstawiciela właściwości utworzonego przez użytkownika dataStore, aby utworzyć instancję argumentu DataStore<T>, gdzie T to typ zdefiniowany w pliku proto. Zadzwoń na najwyższym poziomie pliku kotlin i uzyskać do niego dostęp za pośrednictwem tej usługi. w dalszej części aplikacji. Parametr filename informuje DataStore, w którym plik ma być przechowywany dane, oraz parametr serializer informuje DataStore nazwę klasy serializatora zdefiniowane 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 metody DataStore.data, aby udostępnić właściwość 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 zapewnia updateData() która transakcyjnie aktualizuje zapisany obiekt. updateData() daje Ci bieżącego stanu danych jako instancji typu danych i aktualizuje danych transakcyjnych w ramach niepodzielnej 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()));

Użyj DataStore w kodzie synchronicznym

Jedną z głównych zalet DataStore jest asynchroniczny interfejs API, zawsze istnieje możliwość zmiany otaczającego kodu na asynchroniczny. Ten dla osób, które pracują z dotychczasową bazą kodu, która używa synchronicznego wejścia/wyjścia dysku lub jeśli istnieje zależność, która nie udostępnia do asynchronicznego interfejsu API.

Współrzędy Kotlina zapewniają runBlocking() narzędzie do tworzenia współzależności, które pomaga wypełnić lukę między synchroniczną i asynchroniczną w kodzie. Za pomocą runBlocking() możesz synchronicznie odczytywać dane z DataStore. RxJava oferuje metody blokowania w przeglądarce Flowable. Wywołanie blokuje poniższy kod do momentu zwrócenia 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 spowodować Błędy ANR lub zacinanie się interfejsu. Aby rozwiązać te problemy, możesz asynchronicznie wstępnie wczytać 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();

Dzięki temu DataStore asynchronicznie odczytuje dane i zapisuje je w pamięci podręcznej. Później odczyty synchroniczne z użyciem funkcji runBlocking() mogą być szybsze lub mogą unikać wejścia/wyjścia dysku po zakończeniu początkowego odczytu.

Użyj DataStore w kodzie wieloprocesowym

Możesz skonfigurować magazyn danych 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 DataStore gwarantuje, że:

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

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

  1. Usługa działa w oddzielnym 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 interfejs użytkownika

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

Aby używać magazynu danych w różnych procesach, musisz utworzyć obiekt DataStore za pomocą interfejsu 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ą dla serializatora, która ma być używana, jeśli nie utworzono jeszcze żadnego pliku. 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ć zależności Hilt wstrzyknięcie, aby zapewnić niepowtarzalność instancji DataStore dla każdego procesu:

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

Prześlij opinię

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

Narzędzie do śledzenia błędów
Zgłoś problemy, abyśmy mogli je naprawić.

Dodatkowe materiały

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

Próbki

Blogi

Ćwiczenia z programowania

. .