DataStore   Parte di Android Jetpack.

Prova con la multipiattaforma Kotlin
La multipiattaforma Kotlin consente di condividere il livello dati con altre piattaforme. Scopri come configurare e utilizzare DataStore in KMP

Jetpack DataStore è una soluzione di archiviazione dei dati che consente di archiviare coppie chiave-valore o oggetti digitati con buffer di protocollo. DataStore utilizza le coroutine e i flussi Kotlin per archiviare i dati in modo asincrono, coerente e transazionale.

Se utilizzi SharedPreferences per archiviare i dati, valuta la possibilità di eseguire la migrazione a DataStore.

API DataStore

L'interfaccia DataStore fornisce la seguente API:

  1. Un flusso che può essere utilizzato per leggere i dati da DataStore

    val data: Flow<T>
    
  2. Una funzione per aggiornare i dati in DataStore

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

Configurazioni di DataStore

Se vuoi archiviare e accedere ai dati utilizzando le chiavi, utilizza l'implementazione di Preferences DataStore, che non richiede uno schema predefinito e non fornisce la sicurezza dei tipi. Ha un'API simile a SharedPreferences, ma non presenta gli svantaggi associati alle preferenze condivise.

DataStore consente di rendere persistenti le classi personalizzate. Per farlo, devi definire uno schema per i dati e fornire un Serializer per convertirli in un formato persistente. Puoi scegliere di utilizzare buffer di protocollo, JSON o qualsiasi altra strategia di serializzazione.

Configurazione

Per utilizzare Jetpack DataStore nella tua app, aggiungi quanto segue al file Gradle a seconda dell'implementazione che vuoi utilizzare:

Datastore delle preferenze

Aggiungi le seguenti righe alla sezione delle dipendenze del file 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")
    }
    

Per aggiungere il supporto facoltativo di RxJava, aggiungi le seguenti dipendenze:

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

Aggiungi le seguenti righe alla sezione delle dipendenze del file 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")
    }
    

Aggiungi le seguenti dipendenze facoltative per il supporto di 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")
    }
    

Per serializzare i contenuti, aggiungi le dipendenze per la serializzazione di Protocol Buffer o JSON.

Serializzazione JSON

Per utilizzare la serializzazione JSON, aggiungi quanto segue al file Gradle:

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")
    }
    

Serializzazione Protobuf

Per utilizzare la serializzazione Protobuf, aggiungi quanto segue al file 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")
                }
            }
        }
    }
    

Utilizza DataStore correttamente

Per utilizzare DataStore correttamente, tieni sempre presente le seguenti regole:

  1. Non creare mai più di un'istanza di DataStore per un determinato file in lo stesso processo. In caso contrario, tutte le funzionalità di DataStore potrebbero non funzionare. Se nello stesso processo sono attivi più DataStore per un determinato file, DataStore genererà IllegalStateException durante la lettura o l'aggiornamento dei dati.

  2. Il tipo generico di DataStore<T> deve essere immutabile. La modifica di un tipo utilizzato in DataStore invalida la coerenza fornita da DataStore e crea bug potenzialmente gravi e difficili da rilevare. Ti consigliamo di utilizzare i buffer di protocollo, che contribuiscono a garantire l'immutabilità, un'API chiara e una serializzazione efficiente.

  3. Non combinare gli utilizzi di SingleProcessDataStore e MultiProcessDataStore per lo stesso file. Se intendi accedere al DataStore da più di un processo, devi utilizzare MultiProcessDataStore.

Definizione dei dati

Preferences DataStore

Definisci una chiave che verrà utilizzata per rendere persistenti i dati su disco.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

Per JSON DataStore, aggiungi un'annotazione @Serialization ai dati che vuoi rendere persistenti.

@Serializable data class Settings(val exampleCounter: Int)

Definisci una classe che implementa Serializer<T>, dove T è il tipo della classe a cui hai aggiunto l'annotazione precedente. Assicurati di includere un valore predefinito per il serializzatore da utilizzare se non è ancora stato creato alcun file.

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 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())
    }
}

Proto DataStore

L'implementazione di Proto DataStore utilizza DataStore e i buffer di protocollo per rendere persistenti gli oggetti digitati su disco.

Proto DataStore richiede uno schema predefinito in un file proto nella directory app/src/main/proto/. Questo schema definisce il tipo degli oggetti che rendi persistenti in Proto DataStore. Per saperne di più sulla definizione di uno schema proto, consulta la guida al linguaggio protobuf.

Aggiungi un file denominato settings.proto all'interno della cartella src/main/proto:

syntax = "proto3";

option java_package = "com.example.datastoresampleapp";
option java_multiple_files = true;

message Settings {
  int32 counter = 1;
  bool foo = 2;
}

Definisci una classe che implementa Serializer<T>, dove T è il tipo definito nel file proto. Questa classe serializzatore definisce il modo in cui DataStore legge e scrive il tipo di dati. Assicurati di includere un valore predefinito per il serializzatore da utilizzare se non è ancora stato creato alcun file.

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)
    }
}

Crea un DataStore

Devi specificare un nome per il file utilizzato per rendere persistenti i dati.

Preferences DataStore

L'implementazione di Preferences DataStore utilizza le classi DataStore e Preferences per rendere persistenti le coppie chiave-valore su disco. Utilizza il delegato di proprietà creato da preferencesDataStore per creare un'istanza di DataStore<Preferences>. Chiamalo una volta a livello principale del file Kotlin. Accedi a DataStore tramite questa proprietà nel resto dell'applicazione. In questo modo è più facile mantenere DataStore come singleton. Il parametro obbligatorio name è il nome di Preferences DataStore.

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

JSON DataStore

Utilizza il delegato di proprietà creato da dataStore per creare un'istanza di DataStore<T>, dove T è la classe di dati serializzabile. Chiamalo una volta a livello principale del file Kotlin e accedi tramite questo delegato di proprietà nel resto dell'app. Il parametro fileName indica a DataStore quale file utilizzare per archiviare i dati, mentre il parametro serializer indica a DataStore il nome della classe serializzatore definita in precedenza.

val Context.dataStore: DataStore<Settings> by
    dataStore(
        fileName = "settings.json",
        serializer = SettingsSerializer,
        scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
    )

Proto DataStore

Utilizza il delegato di proprietà creato da dataStore per creare un'istanza di DataStore<T>, dove T è il tipo definito nel file proto. Chiamalo una volta a livello principale del file Kotlin e accedi tramite questo delegato di proprietà nel resto dell'app. Il parametro fileName indica a DataStore quale file utilizzare per archiviare i dati, mentre il parametro serializer indica a DataStore il nome della classe serializzatore definita in precedenza.

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

Leggi da DataStore

Devi specificare un nome per il file utilizzato per rendere persistenti i dati.

Preferences DataStore

Poiché Preferences DataStore non utilizza uno schema predefinito, devi utilizzare la funzione del tipo di chiave corrispondente per definire una chiave per ogni valore che devi archiviare nell'istanza DataStore<Preferences>. Ad esempio, per definire una chiave per un valore int, utilizza intPreferencesKey. Quindi, utilizza la DataStore.data proprietà per esporre il valore archiviato appropriato utilizzando un flusso.

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

JSON DataStore

Utilizza DataStore.data per esporre un Flow della proprietà appropriata dall'oggetto archiviato.

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

Proto DataStore

Utilizza DataStore.data per esporre un Flow della proprietà appropriata dall'oggetto archiviato.

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

Utilizza collectAsStateWithLifecycle per utilizzare il Flow prodotto da un ViewModel in un componibile. In questo modo, il flusso di DataStore viene convertito in modo sicuro in uno stato di Compose che attiva la ricomposizione.

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

Per saperne di più su collectAsStateWithLifecycle, consulta Stato e Jetpack Compose.

Scrivi in DataStore

DataStore fornisce una funzione updateData che aggiorna in modo transazionale un oggetto archiviato. updateData ti fornisce lo stato attuale dei dati come istanza del tipo di dati e aggiorna i dati in modo transazionale in un'operazione atomica di lettura-scrittura-modifica. Tutto il codice nel blocco updateData viene trattato come una singola transazione.

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.toBuilder().setCounter(settings.counter + 1).build()
    }
}

Utilizza DataStore in un'app Compose

Per utilizzare DataStore in un'app Compose, segui le linee guida dell'architettura delle app per Android mantenendo le operazioni di DataStore nel livello dati (ad esempio un repository) ed esponendo i dati all'UI tramite un ViewModel.

Evita di leggere o scrivere direttamente in DataStore all'interno delle funzioni componibili.

  1. Esporre DataStore tramite un ViewModel. Passa il repository (che esegue il wrapping di DataStore) al ViewModel e converti il Flow in un StateFlow in modo che l'UI possa osservarlo, come mostrato nel seguente snippet:

    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. Osserva e scrivi dal tuo componibile. Utilizza collectAsStateWithLifecycle per osservare in modo sicuro StateFlow nell'UI e chiama le funzioni ViewModel per gestire le scritture, come mostrato nel seguente snippet:

    @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")
            }
        }
    }
    

Utilizza DataStore nel codice multiprocesso

Puoi configurare DataStore per accedere agli stessi dati in processi diversi con le stesse proprietà di coerenza dei dati di un singolo processo. In particolare, DataStore fornisce le seguenti proprietà:

  • Le letture restituiscono solo i dati resi persistenti su disco.
  • Coerenza read-after-write.
  • Le scritture vengono serializzate.
  • Le letture non vengono mai bloccate dalle scritture.

Considera un'applicazione di esempio con un servizio e un'attività in cui il servizio viene eseguito in un processo separato e aggiorna periodicamente DataStore.

Questo esempio utilizza un JSON DataStore, ma puoi utilizzare anche un Preferences o un Proto DataStore.

@Serializable data class Time(val lastUpdateMillis: Long)

Un serializzatore indica a DataStore come leggere e scrivere il tipo di dati. Assicurati di includere un valore predefinito per il serializzatore da utilizzare se non è ancora stato creato alcun file. Di seguito è riportato un esempio di implementazione che utilizza kotlinx.serialization:

object TimeSerializer : Serializer<Time> {

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

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

Per poter utilizzare DataStore in processi diversi, devi creare l'oggetto DataStore utilizzando MultiProcessDataStoreFactory sia per il codice dell'app che per quello del servizio:

val dataStore =
    MultiProcessDataStoreFactory.create(
        serializer = TimeSerializer,
        produceFile = { context.dataStoreFile("time.pb") },
        corruptionHandler = null,
    )

Aggiungi quanto segue a AndroidManifiest.xml:

<service
    android:name="com.example.datastore.snippets.TimestampUpdateService"
    android:exported="false"
    android:process=":service" />

Il servizio chiama periodicamente updateLastUpdateTime, che scrive nel datastore utilizzando updateData.

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

L'app legge il valore scritto dal servizio utilizzando il flusso di dati:

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

Ora possiamo combinare tutte queste funzioni in una classe denominata MultiProcessDataStore e utilizzarla in un'app.

Ecco il codice del servizio:

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()
    }
}

E il codice dell'app:

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) }
}

Puoi utilizzare l'iniezione delle dipendenze Hilt in modo che l'istanza di DataStore sia univoca per processo:

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

Gestisci il danneggiamento dei file

In rare occasioni, il file persistente su disco di DataStore potrebbe danneggiarsi. Per impostazione predefinita, DataStore non esegue automaticamente il ripristino in caso di danneggiamento e i tentativi di lettura generano un'eccezione CorruptionException.

DataStore offre un'API di gestione del danneggiamento che può aiutarti a eseguire il ripristino in modo graduale in questo scenario ed evitare di generare l'eccezione. Quando è configurato, il gestore del danneggiamento sostituisce il file danneggiato con uno nuovo contenente un valore predefinito predefinito.

Per configurare questo gestore, fornisci un corruptionHandler quando crei l'istanza di DataStore in by dataStore o nel metodo factory DataStoreFactory:

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

Backup e ripristino del dispositivo

I file di Preferences DataStore (*.preferences_pb) e i file di Proto DataStore vengono archiviati nella directory files/datastore/ dell'app. Per impostazione predefinita, questi file sono inclusi nel backup automatico Android Auto Backup e nei trasferimenti da dispositivo a dispositivo (D2D).

Configura le regole di backup per DataStore

Se DataStore contiene preferenze non sensibili (ad esempio il tema utente o i flag delle funzionalità) insieme a dati sensibili, separali in file DataStore distinti e configura res/xml/data_extraction_rules.xml:

<data-extraction-rules>
    <cloud-backup>
        <!-- Include general settings -->
        <include domain="file" path="datastore/user_settings.preferences_pb"/>
        <!-- Exclude sensitive local state -->
        <exclude domain="file" path="datastore/secure_state.preferences_pb"/>
    </cloud-backup>
    <device-to-device>
        <!-- Transfer settings during device-to-device transfer -->
        <include domain="file" path="datastore/"/>
    </device-to-device>
</data-extraction-rules>

Invia il tuo feedback

Condividi con noi il tuo feedback e le tue idee tramite queste risorse:

Issue Tracker:
Segnala i problemi in modo che possiamo correggere i bug.

Risorse aggiuntive

Per saperne di più su Jetpack DataStore, consulta le seguenti risorse aggiuntive:

Esempi

Blog

Codelab