Progetto: /architecture/_project.yaml Libro: /architecture/_book.yaml parole chiave: datastore, architecture, api:JetpackDataStore description: Explore this app architecture guide on data layer libraries to learn about Preferences DataStore and Proto DataStore, Setup, and more. hide_page_heading: true
DataStore Parte di Android Jetpack.
Jetpack DataStore è una soluzione di archiviazione dei dati che ti consente di archiviare coppie chiave-valore o oggetti digitati con protocol buffer. DataStore utilizza le coroutine Kotlin e Flow 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:
Un flusso che può essere utilizzato per leggere i dati da DataStore
val data: Flow<T>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 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 Protocol Buffers, JSON o qualsiasi altra strategia di serializzazione.
Configura
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.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") }
Per aggiungere il supporto facoltativo di RxJava, aggiungi le seguenti dipendenze:
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
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.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") }
Aggiungi le seguenti dipendenze facoltative per il supporto di 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") }
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") } } } }
Utilizzare correttamente DataStore
Per utilizzare DataStore correttamente, tieni sempre presente le seguenti regole:
Non creare mai più di un'istanza di
DataStoreper un determinato file nello stesso processo. In questo modo, tutte le funzionalità di DataStore potrebbero non funzionare più. Se sono attivi più DataStore per un determinato file nello stesso processo, DataStore genereràIllegalStateExceptiondurante la lettura o l'aggiornamento dei dati.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 individuare. Ti consigliamo di utilizzare i buffer del protocollo, che contribuiscono a garantire l'immutabilità, un'API chiara e una serializzazione efficiente.Non combinare gli utilizzi di
SingleProcessDataStoreeMultiProcessDataStoreper lo stesso file. Se intendi accedere aDataStoreda più di un processo, devi utilizzareMultiProcessDataStore.
Definizione dei dati
Datastore delle preferenze
Definisci una chiave che verrà utilizzata per rendere persistenti i dati sul disco.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
JSON DataStore
Per l'archivio dati JSON, 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 di
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>(
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
L'implementazione di Proto DataStore utilizza DataStore e protocol buffer per salvare gli oggetti tipizzati su disco.
Proto DataStore richiede uno schema predefinito in un file proto nella directory
app/src/main/proto/. Questo schema definisce il tipo per gli oggetti
che vengono archiviati nel tuo Proto DataStore. Per scoprire 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.datastore.snippets.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Definisci una classe che implementa Serializer<T>, dove T è il tipo definito
nel file proto. Questa classe di serializzazione 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.
Datastore delle preferenze
L'implementazione di Preferences DataStore utilizza le classi DataStore e
Preferences per rendere persistenti le coppie chiave-valore su disco. Utilizza il delegato della proprietà creato da preferencesDataStore per creare un'istanza di DataStore<Preferences>. Chiamalo una volta al livello superiore del file Kotlin. Accedi a DataStore tramite questa proprietà per il resto della tua
applicazione. In questo modo è più facile mantenere DataStore come singleton.
In alternativa, utilizza RxPreferenceDataStoreBuilder se utilizzi RxJava.
Il parametro obbligatorio name è il nome del datastore delle preferenze.
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
JSON DataStore
Utilizza il delegato della proprietà creato da dataStore per creare un'istanza di
DataStore<T>, dove T è la classe di dati serializzabile. Chiamalo una volta
al livello superiore del file Kotlin e accedi tramite questa proprietà
delegata 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 del serializzatore definita nel passaggio 1.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.json",
serializer = SettingsSerializer,
)
Proto DataStore
Utilizza il delegato della proprietà creato da dataStore per creare un'istanza di
DataStore<T>, dove T è il tipo definito nel file proto. Chiamalo
una volta al livello superiore del file Kotlin e accedi tramite questa proprietà
delegata 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 del serializzatore definita nel passaggio 1.
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.
Datastore delle preferenze
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 proprietà
DataStore.data per esporre il valore memorizzato 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
memorizzato.
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
memorizzato.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
settings.exampleCounter
}
Scrivi in DataStore
DataStore fornisce una funzione updateData() che aggiorna in modo transazionale un oggetto archiviato. updateData fornisce lo stato attuale dei dati come
istanza del tipo di dati e aggiorna i dati in modo transazionale in un'operazione
di lettura-scrittura-modifica atomica. Tutto il codice nel blocco updateData viene
trattato come una singola transazione.
Datastore delle preferenze
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 }
}
}
Esempio di composizione
Puoi combinare queste funzioni in una classe e utilizzarle in un'app Compose.
Datastore delle preferenze
Ora possiamo inserire queste funzioni in una classe chiamata PreferencesDataStore e
utilizzarla in un'app 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
Ora possiamo inserire queste funzioni in una classe chiamata JSONDataStore e utilizzarla
in un'app 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
Ora possiamo inserire queste funzioni in una classe chiamata ProtoDataStore e utilizzarla
in un'app 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")
}
Utilizzare DataStore nel codice sincrono
Uno dei principali vantaggi di DataStore è l'API asincrona, ma potrebbe non essere sempre possibile modificare il codice circostante in modo che sia asincrono. Questo potrebbe verificarsi se utilizzi una base di codice esistente che utilizza I/O del disco sincrono o se hai una dipendenza che non fornisce un'API asincrona.
Le coroutine Kotlin forniscono il builder di coroutine runBlocking() per colmare il divario tra codice sincrono e asincrono. Puoi utilizzare
runBlocking() per leggere i dati da DataStore in modo sincrono. RxJava offre
metodi di blocco su Flowable. I seguenti blocchi di codice bloccano il thread chiamante
finché DataStore non restituisce i dati:
Kotlin
val exampleData = runBlocking { context.dataStore.data.first() }
Java
Settings settings = dataStore.data().blockingFirst();
L'esecuzione di operazioni di I/O sincrone sul thread UI può causare errori ANR o UI che non risponde. Puoi mitigare questi problemi precaricando in modo asincrono i dati da DataStore:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
context.dataStore.data.first()
// You should also handle IOExceptions here.
}
}
Java
dataStore.data().first().subscribe();
In questo modo, DataStore legge i dati in modo asincrono e li memorizza nella cache in memoria. Le letture
sincrone successive che utilizzano runBlocking() potrebbero essere più veloci o evitare del tutto un'operazione
di I/O del disco se la lettura iniziale è stata completata.
Utilizzare 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 letture restituiscono solo i dati salvati su disco.
- Coerenza read-after-write.
- Le scritture vengono serializzate.
- Le letture non vengono mai bloccate dalle scritture.
Prendi in considerazione 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 datastore JSON, ma puoi utilizzare anche un datastore delle preferenze o proto.
@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>(
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()
)
}
}
Per poter utilizzare DataStore in diversi processi, devi creare
l'oggetto DataStore utilizzando MultiProcessDataStoreFactory sia per l'app
sia per il codice del servizio:
val dataStore = MultiProcessDataStoreFactory.create(
serializer = TimeSerializer,
produceFile = {
File("${context.cacheDir.path}/time.pb")
},
corruptionHandler = null
)
Aggiungi quanto segue a AndroidManifiest.xml:
<service
android:name=".TimestampUpdateService"
android:process=":my_process_id" />
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 chiamata
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'inserimento delle dipendenze Hilt in modo che l'istanza DataStore sia univoca per processo:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
Gestire il danneggiamento dei file
In rari casi, il file persistente su disco di DataStore potrebbe
danneggiarsi. Per impostazione predefinita, DataStore non esegue automaticamente il recupero in caso di danneggiamento e i tentativi di lettura causeranno la generazione di un errore CorruptionException.
DataStore offre un'API di gestione del danneggiamento che può aiutarti a eseguire il recupero in modo controllato in questo scenario ed evitare di generare l'eccezione. Se 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 DataStore in by dataStore() o nel metodo factory DataStoreFactory:
val dataStore: DataStore<Settings> = DataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
Fornisci feedback
Condividi con noi il tuo feedback e le tue idee tramite queste risorse:
- Issue Tracker:
- Segnala i problemi per consentirci di correggere i bug.
Risorse aggiuntive
Per saperne di più su Jetpack DataStore, consulta le seguenti risorse aggiuntive:
Campioni
Blog
Codelab
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Caricare e visualizzare i dati impaginati
- Panoramica di LiveData
- Layout ed espressioni di binding