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:
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 DataStoreIllegalStateException
.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ę.Nigdy nie mieszaj atrybutów
SingleProcessDataStore
iMultiProcessDataStore
w tym samym pliku. Jeśli chcesz uzyskać dostęp doDataStore
z większej liczby procesów, zawsze używajMultiProcessDataStore
.
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 DataStore
i Preferences
, 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:
- Zdefiniuj klasę, która implementuje
Serializer<T>
, gdzieT
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. - Użyj obiektu zastępczego właściwości utworzonego przez
dataStore
, aby utworzyć instancjęDataStore<T>
, gdzieT
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. Parametrfilename
informuje DataStore, którego pliku należy użyć do przechowywania danych, a parametrserializer
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ą:
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) } } }
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
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Wczytywanie i wyświetlanie danych podzielonych na strony
- Omówienie LiveData
- Układy i wyrażenia wiążące