DataStore, część Androida Jetpack.

Wypróbuj Kotlin Multiplatform
Kotlin Multiplatform (KMP) umożliwia udostępnianie warstwy danych innym platformom. Dowiedz się, jak skonfigurować DataStore i z niej korzystać 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 do przechowywania danych używasz SharedPreferences, rozważ migrację do DataStore.

DataStore API

Interfejs DataStore udostępnia ten interfejs API:

  1. Przepływ, którego można użyć do odczytywania danych z Datastore

    val data: Flow<T>
    
  2. Funkcja aktualizowania danych w DataStore

    suspend updateData(transform: suspend (t) -> T)
    

Konfiguracje magazynu danych

Jeśli chcesz przechowywać dane i uzyskiwać do nich dostęp za pomocą kluczy, użyj implementacji Preferences DataStore, która nie wymaga wstępnie zdefiniowanego schematu i nie zapewnia bezpieczeństwa typów. Ma interfejs API podobny do SharedPreferences, ale nie ma wad związanych z ustawieniami współdzielonymi.

DataStore umożliwia zapisywanie klas niestandardowych. Aby to zrobić, musisz zdefiniować schemat danych i podać funkcję Serializer, która przekonwertuje je na format trwały. Możesz użyć buforów protokołu, JSON-a lub dowolnej innej strategii serializacji.

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

Dodaj te wiersze do sekcji zależności w pliku Gradle:

Groovy

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

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.1.7"
    }
    

Kotlin

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

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.1.7")
    }
    

Aby dodać opcjonalną obsługę RxJava, dodaj te zależności:

Groovy

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

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

Kotlin

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

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

DataStore

Dodaj te wiersze do sekcji zależności w pliku Gradle:

Groovy

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.1.7"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.1.7"
    }
    

Kotlin

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.1.7")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.1.7")
    }
    

Dodaj te opcjonalne zależności, aby włączyć obsługę RxJava:

Groovy

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

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

Kotlin

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

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

Aby serializować treści, dodaj zależności dla serializacji w formacie Protocol Buffers lub JSON.

Serializacja JSON

Aby używać serializacji JSON, dodaj do pliku Gradle ten ciąg:

Groovy

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Kotlin

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

Serializacja buforów protokołu

Aby używać serializacji Protobuf, dodaj do pliku Gradle:

Groovy

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Kotlin

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

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 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ę.

  3. Nie mieszaj zastosowań atrybutów SingleProcessDataStore i MultiProcessDataStore w tym samym pliku. Jeśli zamierzasz uzyskać dostęp do DataStore z więcej niż jednego procesu, musisz użyć MultiProcessDataStore.

Definicja danych

Preferences DataStore

Zdefiniuj klucz, który będzie używany do zapisywania danych na dysku.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

W przypadku magazynu danych JSON dodaj do danych, które chcesz zachować, adnotację @Serialization.

@Serializable
data class Settings(
    val exampleCounter: Int
)

Zdefiniuj klasę, która implementuje Serializer<T>, gdzie T jest typem klasy, do której dodano wcześniejszą adnotację. Pamiętaj, aby uwzględnić wartość domyślną serializatora, która będzie używana, jeśli nie utworzono jeszcze pliku.

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 0)

    override suspend fun readFrom(input: InputStream): Settings =
        try {
            Json.decodeFromString<Settings>(
                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(t)
                .encodeToByteArray()
        )
    }
}

Proto DataStore

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

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.

Dodaj plik o nazwie settings.proto w folderze src/main/proto:

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Zdefiniuj klasę, która implementuje Serializer<T>, gdzie T to typ zdefiniowany w pliku proto. Ta klasa serializatora określa, jak DataStore odczytuje i zapisuje typ danych. Pamiętaj, aby podać wartość domyślną serializatora, która będzie używana, jeśli nie ma jeszcze utworzonego pliku.

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) {
        return t.writeTo(output)
    }
}

Tworzenie magazynu danych

Musisz podać nazwę pliku, który będzie używany do przechowywania danych.

Preferences DataStore

Implementacja Preferences DataStore używa klas DataStorePreferences do zapisywania par klucz-wartość na dysku. Użyj delegata właściwości utworzonego przez preferencesDataStore, aby utworzyć instancję DataStore<Preferences>. Wywołaj ją raz na najwyższym poziomie pliku Kotlin. Dostęp do Datastore w tej usłudze w pozostałej części aplikacji. Ułatwia to utrzymanie DataStore jako pojedynczego obiektu. Jeśli używasz RxJava, możesz też użyć RxPreferenceDataStoreBuilder. Obowiązkowy parametr name to nazwa Preferences DataStore.

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

JSON DataStore

Użyj delegata właściwości utworzonego przez dataStore, aby utworzyć instancję DataStore<T>, gdzie T jest klasą danych z możliwością serializacji. Wywołaj go raz na najwyższym poziomie pliku Kotlin i uzyskaj do niego dostęp za pomocą tego delegata właściwości w pozostałej części aplikacji. Parametr fileName informuje DataStore, którego pliku ma używać do przechowywania danych, a parametr serializer informuje DataStore o nazwie klasy serializatora zdefiniowanej w kroku 1.

val Context.dataStore: DataStore<Settings> by dataStore(
    fileName = "settings.json",
    serializer = SettingsSerializer,
)

Proto DataStore

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 ją raz na najwyższym poziomie pliku Kotlin i uzyskuj do niej dostęp za pomocą tego delegata właściwości w pozostałej części aplikacji. Parametr fileName informuje DataStore, którego pliku ma używać do przechowywania danych, a parametr serializer informuje DataStore o nazwie klasy serializatora zdefiniowanej w kroku 1.

val Context.dataStore: DataStore<Settings> by dataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer,
)

Odczyt z magazynu danych

Musisz podać nazwę pliku, który będzie używany do przechowywania danych.

Preferences DataStore

Magazyn danych Preferences DataStore nie korzysta ze wstępnie zdefiniowanego schematu, dlatego musisz użyć odpowiedniej funkcji typu klucza, aby zdefiniować klucz dla każdej wartości, którą chcesz przechowywać w instancji DataStore<Preferences>. Aby na przykład zdefiniować klucz dla wartości całkowitej, użyj intPreferencesKey(). Następnie użyj właściwości DataStore.data, aby udostępnić odpowiednią zapisaną wartość za pomocą Flow.

fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
    preferences[EXAMPLE_COUNTER] ?: 0
}

JSON DataStore

Użyj DataStore.data, aby udostępnić Flow odpowiedniej właściwości z przechowywanego obiektu.

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Proto DataStore

Użyj DataStore.data, aby udostępnić Flow odpowiedniej właściwości z przechowywanego obiektu.

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Zapisywanie w DataStore

DataStore udostępnia funkcję updateData(), która transakcyjnie aktualizuje przechowywany obiekt. 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. Cały kod w bloku updateData jest traktowany jako pojedyncza transakcja.

Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

JSON DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy { exampleCounter = exampleCounter + 1 }
    }
}

Przykładowa kompozycja

Możesz połączyć te funkcje w klasie i używać jej w aplikacji Compose.

Preferences DataStore

Możemy teraz umieścić te funkcje w klasie o nazwie PreferencesDataStore i używać jej w aplikacji Compose.

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }

// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(
    onClick = {
        coroutineScope.launch { preferencesDataStore.incrementCounter() }
    }
) {
    Text("increment")
}

JSON DataStore

Możemy teraz umieścić te funkcje w klasie o nazwie JSONDataStore i używać jej w aplikacji Compose.

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }

// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
    Text("increment")
}

Proto DataStore

Możemy teraz umieścić te funkcje w klasie o nazwie ProtoDataStore i używać jej w aplikacji Compose.

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }

// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
    Text("increment")
}

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.

W rutynach Kotlin dostępny jest konstruktor rutyn 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 na 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ż przykładową aplikację z usługą i aktywnością, w której usługa działa w osobnym procesie i okresowo aktualizuje DataStore.

W tym przykładzie używamy magazynu danych JSON, ale możesz też użyć magazynu danych preferencji lub magazynu danych protokołu.

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

Serializator 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 ma jeszcze utworzonego pliku. Oto przykładowa implementacja z użyciem biblioteki kotlinx.serialization:

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

    override suspend fun readFrom(input: InputStream): Time =
        try {
            Json.decodeFromString<Time>(
                input.readBytes().decodeToString()
            )
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read Time", serialization)
        }

    override suspend fun writeTo(t: Time, output: OutputStream) {
        output.write(
            Json.encodeToString(t)
                .encodeToByteArray()
        )
    }
}

Aby móc używać DataStore w różnych procesach, musisz utworzyć obiekt DataStore za pomocą MultiProcessDataStoreFactory zarówno w przypadku aplikacji, jak i kodu usługi:

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.cacheDir.path}/time.pb")
    },
    corruptionHandler = null
)

Dodaj do pliku AndroidManifiest.xml te informacje:

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

Usługa okresowo wywołuje funkcję updateLastUpdateTime(), która zapisuje dane w pamięci za pomocą funkcji updateData.

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time ->
        time.copy(lastUpdateMillis = System.currentTimeMillis())
    }
}

Aplikacja odczytuje wartość zapisaną przez usługę za pomocą tego przepływu danych:

fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
    time.lastUpdateMillis
}

Teraz możemy połączyć wszystkie te funkcje w klasie o nazwie MultiProcessDataStore i używać jej w aplikacji.

Oto kod usługi:

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

oraz kod aplikacji:

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Last updated: $lastUpdateTime",
    fontSize = 25.sp
)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose {
        context.stopService(serviceIntent)
    }
}

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ą 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 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