Architektura aplikacji: warstwa danych – DataStore – Android Developers

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.

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

  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

Ponieważ usługa Preferences DataStore nie korzysta z 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 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 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