DataStoreCzęść Androida Jetpack.

Wypróbuj Kotlin Multiplatform
Kotlin Multiplatform umożliwia udostępnianie warstwy danych innym platformom. Dowiedz się, jak skonfigurować i używać DataStore w KMP

Jetpack DataStore to rozwiązanie do przechowywania danych, które umożliwia zapisywanie par klucz-wartość lub obiektów z określonym typem za pomocą buforów protokołu. DataStore używa w tym celu funkcji Kotlin Coroutines i Flow, aby przechowywać dane asynchronicznie, spójnie i transakcyjnie.

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

Preferences DataStore i Proto DataStore

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

  • Preferences DataStore przechowuje dane i uzyskuje do nich dostęp 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 DataStore

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

  1. Nigdy nie twórz więcej niż 1 instancji DataStore dla danego pliku w tym samym procesie. Może to spowodować nieprawidłowe działanie wszystkich funkcji DataStore. Jeśli w tym samym procesie dla danego pliku jest aktywnych kilka obiektów DataStore, podczas odczytywania lub aktualizowania danych obiekt DataStore zgłosi błąd IllegalStateException.

  2. Typ ogólny elementu DataStore<T> musi być niezmienny. Modyfikowanie typu używanego w DataStore unieważnia wszelkie gwarancje, jakie zapewnia DataStore, i może powodować poważne, trudne do wykrycia błędy. Zdecydowanie zalecamy używanie buforów protokołu, które zapewniają niezmienność, prosty interfejs API i wydajną serializację.

  3. Nigdy nie mieszaj użycia atrybutów SingleProcessDataStore i MultiProcessDataStore w tym samym pliku. Jeśli zamierzasz uzyskać dostęp do DataStore z więcej niż jednego procesu, zawsze używaj MultiProcessDataStore.

Konfiguracja

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

Magazyn danych preferencji

Groovy

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

Przechowywanie par klucz-wartość za pomocą Preferences DataStore

Implementacja Preferences DataStore używa klas DataStorePreferences do zapisywania na dysku prostych par klucz-wartość.

Tworzenie magazynu danych Preferences

Użyj delegata właściwości utworzonego przez preferencesDataStore, aby utworzyć instancję DataStore<Preferences>. Wywołaj go raz na najwyższym poziomie pliku Kotlin i uzyskaj do niego dostęp za pomocą tej właściwości w pozostałej części aplikacji. Ułatwia to utrzymanie DataStore jako pojedynczego elementu. Jeśli używasz RxJava, możesz też użyć polecenia RxPreferenceDataStoreBuilder. Obowiązkowy parametr name to nazwa bazy danych Preferences 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();

Odczytywanie danych z Preferences DataStore

Ponieważ Preferences DataStore 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 int, użyj intPreferencesKey(). Następnie użyj właściwości DataStore.data, aby udostępnić odpowiednią zapisaną wartość 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));

Zapisywanie w Preferences DataStore

Biblioteka Preferences DataStore udostępnia funkcję edit(), która transakcyjnie aktualizuje dane w DataStore. Parametr transform funkcji przyjmuje blok kodu, w którym możesz w razie potrzeby aktualizować wartości. Cały kod w bloku przekształcenia 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 obiektów z określonym typem za pomocą Proto DataStore

Implementacja Proto DataStore wykorzystuje DataStore i bufory protokołu do zapisywania na dysku obiektów z określonym typem.

Określanie schematu

Proto DataStore wymaga wstępnie zdefiniowanego schematu w pliku proto w katalogu app/src/main/proto/. Ten schemat definiuje typ obiektów, które są przechowywane 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.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Tworzenie magazynu danych Proto

Tworzenie magazynu danych Proto do przechowywania obiektów z określonym typem 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 serializatora informuje DataStore, jak odczytywać i zapisywać typ danych. Podaj wartość domyślną serializatora, która będzie używana, jeśli nie ma jeszcze utworzonego pliku.
  2. Użyj delegata właściwości utworzonego przez dataStore, aby utworzyć instancję DataStore<T>, gdzie T to typ zdefiniowany w pliku proto. Wywołaj tę funkcję raz na najwyższym poziomie pliku Kotlin i uzyskaj do niej dostęp za pomocą delegata tej właściwości w pozostałej części aplikacji. Parametr filename informuje DataStore, którego pliku 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 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());

Zapisywanie w magazynie danych protokołu

Proto DataStore udostępnia funkcję updateData() umożliwiającą transakcyjne aktualizowanie przechowywanego obiektu. updateData() zwraca bieżący stan danych jako instancję typu danych i aktualizuje dane w sposób transakcyjny 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ż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 to być konieczne, jeśli pracujesz z istniejącą bazą kodu, która korzysta z synchronicznych operacji wejścia/wyjścia na dysku, lub jeśli masz zależność, która nie udostępnia asynchronicznego interfejsu API.

Korutyny Kotlin udostępniają konstruktor korutyn runBlocking(), który pomaga wypełnić lukę między kodem synchronicznym a asynchronicznym. Możesz używać runBlocking() do synchronicznego odczytywania danych z DataStore. RxJava udostępnia metody blokujące w przypadku Flowable. Poniższy kod blokuje wywołujący wątek do momentu, aż DataStore zwróci dane:

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 może powodować błędy ANR lub zacinanie się interfejsu. Możesz rozwiązać te problemy, asynchronicznie wstępnie 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 zapisuje je w pamięci podręcznej. Późniejsze odczyty synchroniczne za pomocą runBlocking() mogą być szybsze lub całkowicie uniknąć operacji wejścia/wyjścia dysku, jeśli początkowy odczyt został zakończony.

Używanie DataStore w kodzie wieloprocesowym

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

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

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

  1. Usługa działa w osobnym procesie 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ą 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, jeśli nie utworzono jeszcze pliku. Oto przykładowa implementacja z użyciem 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): Settings =
       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 mieć pewność, że instancja DataStore jest unikalna dla każdego procesu:

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

Obsługa uszkodzonych plików

W rzadkich przypadkach trwały plik na dysku DataStore może ulec uszkodzeniu. Domyślnie DataStore nie przywraca automatycznie danych po uszkodzeniu, a próby odczytu z niego powodują zgłoszenie przez system błędu CorruptionException.

DataStore udostępnia interfejs API obsługi uszkodzeń, który może pomóc w bezproblemowym odzyskaniu danych w takiej sytuacji i uniknięciu zgłoszenia wyjątku. Po skonfigurowaniu moduł obsługi uszkodzeń zastępuje uszkodzony plik nowym plikiem zawierającym predefiniowaną wartość domyślną.

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

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

Prześlij opinię

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

Issue Tracker
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

Codelabs