Magazyn danych
Element Jetpacka na Androida.
Zadbaj o dobrą organizację dzięki kolekcji
Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.
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 języku Kotlin 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łów, ale zapewnia bezpieczeństwo typów.
Prawidłowe korzystanie z magazynu danych
Aby prawidłowo korzystać z DataStore, pamiętaj o tych zasadach:
Nigdy nie twórz więcej niż 1 instancji funkcji
DataStore
dla danego pliku w ramach tego samego procesu. 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 gwarancję niezmienności, prosty interfejs API i wydajną serializację.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 kod zależnie od tego, której implementacji chcesz użyć:
Magazyn danych preferencji
// 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" }
// 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
// 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" }
// 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ść w Preferences DataStore
Implementacja Preference DataStore używa klas DataStore
i Preferences
do zapisywania na dysku prostych par klucz-wartość.
Tworzenie magazynu danych ustawień
Aby utworzyć instancję DataStore<Preferences>
, użyj obiektu zastępczego usługi utworzonego przez preferencesDataStore
. Wywołaj ją raz na najwyższym poziomie pliku Kotlin i używaj jej za pomocą tej właściwości w pozostałych częściach aplikacji. Dzięki temu DataStore
będzie łatwiej zachować jako pojedynczy obiekt. Jeśli używasz RxJava, możesz też użyć polecenia RxPreferenceDataStoreBuilder
. Wymagany parametr name
to nazwa DataStore preferencji.
// At the top level of your kotlin file: val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
RxDataStore<Preferences> dataStore = new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();
Odczyt z magazynu danych Preferencji
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ą zapisaną wartość za pomocą atrybutu Flow
.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter") val exampleCounterFlow: Flow<Int> = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0 }
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.
suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 } }
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 korzysta z 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.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Tworzenie magazynu danych Proto
Tworzenie schowu danych Proto na potrzeby przechowywania typowanych obiektów wymaga wykonania 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.
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 )
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 funkcji DataStore.data
, aby ujawnić Flow
odpowiedniej właściwości z przechowywanego obiektu.
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter }
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. Funkcja updateData()
zwraca bieżący stan danych jako instancję typu danych i zmienia dane w ramach transakcji w ramach operacji atomowej odczytu, zapisu i modyfikacji.
suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() } }
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 synchronicznego wejścia/wyjścia na dysk, lub jeśli masz zależność, która nie udostępnia interfejsu API asynchronicznego.
Kotlinowe coroutines zapewniają runBlocking()
twórcę 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:
val exampleData = runBlocking { context.dataStore.data.first() }
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:
override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. } }
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 DataStore.
Jeśli nie ma jeszcze żadnego utworzonego pliku, podaj wartość domyślną dla serializatora, która ma być użyta. 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): 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()
)
}
}
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 plikiem zawierającym zdefiniowaną wstępnie wartość domyślną.
Aby skonfigurować tego przetwarzacza, 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łoś problemy, abyśmy mogli naprawić błędy.
Dodatkowe materiały
Więcej informacji o Jetpack DataStore znajdziesz w tych materiałach:
Próbki
Learn how this app was designed and built in the design case study, architecture learning journey and modularization learning journey.
This is the repository for the Now in Android app. It is a work in progress 🚧.
Now in Android is a fully functionalNow in Android App
Blogi
Ćwiczenia z programowania
Polecane dla Ciebie
Wczytywanie i wyświetlanie danych z podziałem na strony
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.
Omówienie LiveData
Używaj LiveData do obsługi danych w sposób uwzględniający cykl życia.
Układy i wyrażenia powiązania
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.