Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: Zapoznaj się z tym przewodnikiem po architekturze aplikacji dotyczącym bibliotek warstwy danych, aby dowiedzieć się więcej o bibliotekach Preferences DataStore i Proto DataStore, konfiguracji i innych kwestiach. hide_page_heading: true
DataStore, część Androida Jetpack.
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 do przechowywania danych używasz SharedPreferences, 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:
- Nigdy nie twórz więcej niż 1 instancji - DataStoredla 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.
- Typ ogólny elementu - DataStore<T>musi być niezmienny. Zmiana typu używanego w DataStore unieważnia spójność zapewnianą przez DataStore i może powodować poważne, trudne do wykrycia błędy. Zalecamy używanie buforów protokołu, które zapewniają niezmienność, przejrzysty interfejs API i wydajną serializację.
- Nie mieszaj zastosowań atrybutów - SingleProcessDataStorei- MultiProcessDataStorew tym samym pliku. Jeśli zamierzasz uzyskać dostęp do- DataStorez więcej niż jednego procesu, musisz użyć- MultiProcessDataStore.
Konfiguracja
Aby używać Jetpack DataStore w aplikacji, dodaj do pliku Gradle poniższy kod w zależności od tego, której implementacji chcesz użyć:
Preferences DataStore
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 DataStore i Preferences do zapisywania par klucz-wartość na dysku.
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 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, "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 usłudze Preferences DataStore
Biblioteka Preferences DataStore udostępnia funkcję edit(), która transakcyjnie aktualizuje dane w DataStore. Parametr transform funkcji akceptuje 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 używa DataStore i buforów protokołów 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:
- Zdefiniuj klasę, która implementuje Serializer<T>, gdzieTto 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.
- Użyj delegata właściwości utworzonego przez dataStore, aby utworzyć instancjęDataStore<T>, gdzieTto typ zdefiniowany w pliku proto. Wywołaj tę funkcję raz na najwyższym poziomie pliku Kotlin i uzyskaj do niej dostęp za pomocą tego delegata właściwości w pozostałej części aplikacji. Parametrfilenameinformuje DataStore, którego pliku ma używać do przechowywania danych, a parametrserializerokreśla nazwę 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() {
    return Settings.getDefaultInstance();
  }
  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (InvalidProtocolBufferException exception) {
      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żyć
runBlocking(), aby synchronicznie odczytywać dane z usługi DataStore. RxJava udostępnia metody blokujące w Flowable. Poniższy 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 interfejsu może powodować błędy ANR lub brak reakcji 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 właściwościami spójności danych co w ramach jednego procesu. W szczególności DataStore zapewnia:
- 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ą:
- 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) } } }
- 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 uwzględnić wartość domyślną serializatora, która będzie używana, jeśli nie ma jeszcze utworzonego 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 instancja DataStore była unikalna w każdym procesie:
@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ą, że system zgłasza błąd CorruptionException.
DataStore udostępnia interfejs API obsługi uszkodzeń, który może pomóc w 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 wartość 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:
- Narzędzie do śledzenia problemó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
Codelabs
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Wczytywanie i wyświetlanie danych podzielonych na strony
- Omówienie LiveData
- Układy i wyrażenia wiążące
 
  