DataStore   Część Androida Jetpack.

Wypróbuj Kotlin Multiplatform
Kotlin Multiplatform umożliwia udostępnianie warstwy danych innym platformom. Dowiedz się, jak skonfigurować DataStore i z niego korzystać w KMP

Jetpack DataStore to rozwiązanie do przechowywania danych, które umożliwia przechowywanie par klucz-wartość lub obiektów z określonymi typami za pomocą buforów protokołów. DataStore używa korutyn Kotlin i Flow do asynchronicznego, spójnego i transakcyjnego przechowywania danych.

Jeśli do przechowywania danych używasz SharedPreferences, rozważ przejście na DataStore.

DataStore API

Interfejs DataStore udostępnia ten interfejs API:

  1. Flow, którego można używać do odczytywania danych z DataStore.

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

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

Konfiguracje DataStore

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 preferencjami współdzielonymi.

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

Konfiguracja

Aby używać Jetpack DataStore w aplikacji, dodaj do pliku Gradle te zależności, 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.2.1"

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

Kotlin

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

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

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

Groovy

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

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

Kotlin

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

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

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.2.1"

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

Kotlin

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

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

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

Groovy

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

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

Kotlin

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

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

Aby serializować treści, dodaj zależności serializacji 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 używanie DataStore

Aby prawidłowo używać DataStore, zawsze pamiętaj o tych zasadach:

  1. W tym samym procesie nigdy nie twórz więcej niż 1 instancji DataStore dla danego pliku. Może to spowodować przerwanie działania wszystkich funkcji DataStore. Jeśli w tym samym procesie jest aktywnych kilka DataStore dla danego pliku, DataStore zgłosi wyjątek IllegalStateException podczas odczytywania lub aktualizowania danych.

  2. Typ ogólny DataStore<T> musi być niezmienny. Modyfikowanie typu używanego w DataStore powoduje utratę spójności zapewnianej przez DataStore i może prowadzić do poważnych, trudnych do wykrycia błędów. Zalecamy używanie buforów protokołów, które pomagają zapewnić niezmienność, przejrzysty interfejs API i wydajną serializację.

  3. Nie mieszaj użycia SingleProcessDataStore i MultiProcessDataStore w przypadku tego samego pliku. Jeśli chcesz uzyskać dostęp do DataStore z więcej niż 1 procesu, musisz użyć MultiProcessDataStore.

Definicja danych

Preferences DataStore

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

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

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

@Serializable
data class Settings(
    val exampleCounter: Int
)

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

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 utrwalania obiektów z określonymi typami na dysku.

Proto DataStore wymaga wstępnie zdefiniowanego schematu w pliku proto w katalogu app/src/main/proto/. Ten schemat określa typ obiektów, które utrwalasz 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 jest typem zdefiniowanym w pliku proto. Ta klasa serializatora określa, jak DataStore odczytuje i zapisuje Twój typ danych. Pamiętaj, aby uwzględnić wartość domyślną serializatora, która będzie używana, jeśli plik nie został jeszcze utworzony.

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 DataStore

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

Preferences DataStore

Implementacja Preferences DataStore używa klas DataStore i Preferences do utrwalania par klucz-wartość na dysku. Aby utworzyć instancję DataStore<Preferences>, użyj delegata właściwości utworzonego przez preferencesDataStore. Wywołaj go raz na najwyższym poziomie pliku Kotlin. W pozostałej części aplikacji uzyskuj dostęp do DataStore za pomocą tej właściwości. Ułatwia to utrzymanie DataStore jako singletonu. 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

Aby utworzyć instancję DataStore<T>, gdzie T jest serializowalną klasą danych, użyj delegata właściwości utworzonego przez dataStore. Wywołaj go raz na najwyższym poziomie pliku Kotlin i uzyskuj 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 wcześniej.

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

Proto DataStore

Aby utworzyć instancję DataStore<T>, gdzie T jest typem zdefiniowanym w pliku proto, użyj delegata właściwości utworzonego przez dataStore. Wywołaj go raz na najwyższym poziomie pliku Kotlin i uzyskuj 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 wcześniej.

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

Odczytywanie z DataStore

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

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.

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 zapisanego 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 zapisanego obiektu.

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

Użyj collectAsStateWithLifecycle, aby wykorzystać Flow wygenerowany przez a ViewModel w komponencie. Bezpiecznie konwertuje to Flow DataStore na stan Compose, który wywołuje rekompozycję.

@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
  val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
  Text(text = "Example counter: ${counter}")
}

Więcej informacji o collectAsStateWithLifecycle, znajdziesz w artykule Stan i Jetpack Compose.

Zapisywanie w DataStore

DataStore udostępnia funkcję updateData, która transakcyjnie aktualizuje zapisany obiekt. updateData udostępnia bieżący stan danych jako instancję typu danych i aktualizuje dane transakcyjnie w ramach atomowej 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 }
    }
}

Używanie DataStore w aplikacji Compose

Aby używać DataStore w aplikacji Compose, postępuj zgodnie z wytycznymi dotyczącymi architektury aplikacji na Androida, umieszczając operacje DataStore w warstwie danych (np. w repozytorium) i udostępniając dane interfejsowi użytkownika za pomocą ViewModel.

Unikaj bezpośredniego odczytywania i zapisywania w DataStore w funkcjach komponentów.

  1. Udostępnij DataStore za pomocą ViewModel. Przekaż repozytorium (które opakowuje DataStore) do ViewModel i przekonwertuj Flow na StateFlow, aby interfejs użytkownika mógł go łatwo obserwować, jak pokazano w tym fragmencie kodu:

    class SettingsViewModel(
        private val userPreferencesRepository: UserPreferencesRepository
    ) : ViewModel() {
    
        // Expose the DataStore flow as a StateFlow for Compose
        val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = UserSettings.getDefaultInstance()
            )
    
        fun updateCounter(newValue: Int) {
            viewModelScope.launch {
                userPreferencesRepository.updateCounter(newValue)
            }
        }
    }
    
  2. Obserwuj i zapisuj z komponentu. Użyj collectAsStateWithLifecycle, aby bezpiecznie obserwować StateFlow w interfejsie użytkownika, i wywołuj funkcje ViewModel, aby obsługiwać zapisy, jak pokazano w tym fragmencie kodu:

    @Composable
    fun SettingsScreen(
        viewModel: SettingsViewModel = viewModel()
    ) {
        // Safely collect the state
        val settings by viewModel.userSettings.collectAsStateWithLifecycle()
    
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Current counter: ${settings.counter}")
    
            Spacer(modifier = Modifier.height(8.dp))
    
            Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) {
                Text("Increment Counter")
            }
        }
    }
    

Używanie DataStore w kodzie wieloprocesowym

Możesz skonfigurować DataStore tak, aby uzyskiwać dostęp do tych samych danych w różnych procesach z takimi samymi właściwościami spójności danych jak w przypadku jednego procesu. DataStore udostępnia te właściwości:

  • Odczyty zwracają tylko dane, które zostały utrwalone na dysku.
  • Spójność odczytu po zapisie.
  • Zapisy 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.

Ten przykład używa magazynu danych JSON, ale możesz też użyć Preferences lub Proto DataStore.

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

Serializator informuje DataStore, jak odczytywać i zapisywać Twój typ danych. Pamiętaj, aby uwzględnić wartość domyślną serializatora, która będzie używana, jeśli plik nie został jeszcze utworzony. Oto przykładowa implementacja z użyciem 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 kodzie aplikacji jak i usługi:

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

Dodaj te elementy do pliku AndroidManifiest.xml:

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

Usługa okresowo wywołuje updateLastUpdateTime, która zapisuje w magazynie danych za pomocą updateData.

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

Aplikacja odczytuje wartość zapisaną przez usługę za pomocą 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()
    }
}

A oto 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ć Hilt wstrzykiwania zależności, aby instancja DataStore była unikalna dla każdego procesu:

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

Obsługa uszkodzenia pliku

W rzadkich przypadkach trwały plik DataStore na dysku może ulec uszkodzeniu. Domyślnie DataStore nie przywraca automatycznie danych po uszkodzeniu, a próby odczytu z niego spowodują zgłoszenie przez system wyjątku CorruptionException.

DataStore udostępnia interfejs API obsługi uszkodzeń, który może pomóc w eleganckim przywróceniu danych w takiej sytuacji i uniknięciu zgłoszenia wyjątku. Po skonfigurowaniu procedura obsługi uszkodzeń zastępuje uszkodzony plik nowym plikiem zawierającym wstępnie zdefiniowaną wartość domyślną.

Aby skonfigurować tę procedurę obsługi, podaj corruptionHandler podczas tworzenia instancji DataStore w by dataStore lub w metodzie fabrycznej DataStoreFactory:

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.filesDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

Przesyłanie opinii

Podziel się z nami swoją opinią i pomysłami, korzystając z tych materiałów:

Issue Tracker:
Zgłaszaj problemy, abyśmy mogli naprawić błędy.

Dodatkowe materiały

Więcej informacji o Jetpack DataStore znajdziesz w tych materiałach:

Przykłady

Blogi

Codelabs