Progetto: /architecture/_project.yaml Libro: /architecture/_book.yaml parole chiave: datastore, architettura, 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.
Preferences DataStore e Proto DataStore
DataStore fornisce due implementazioni diverse: Preferences DataStore e Proto DataStore.
- Preferences DataStore archivia e accede ai dati utilizzando le chiavi. Questa implementazione non richiede uno schema predefinito e non fornisce sicurezza dei tipi.
- Proto DataStore memorizza i dati come istanze di un tipo di dati personalizzato. Questa implementazione richiede di definire uno schema utilizzando i buffer di protocollo, ma fornisce la sicurezza dei tipi.
Utilizzare correttamente DataStore
Per utilizzare DataStore correttamente, tieni sempre presente le seguenti regole:
Non creare mai più di un'istanza di
DataStore
per 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àIllegalStateException
durante 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 protocol buffer, che contribuiscono a garantire l'immutabilità, un'API chiara e una serializzazione efficiente.Non combinare gli utilizzi di
SingleProcessDataStore
eMultiProcessDataStore
per lo stesso file. Se intendi accedere aDataStore
da più di un processo, devi utilizzareMultiProcessDataStore
.
Configura
Per utilizzare Jetpack DataStore nella tua app, aggiungi quanto segue al file Gradle a seconda dell'implementazione che vuoi utilizzare:
Datastore delle preferenze
Groovy
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
Proto DataStore
Groovy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.7" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.7") }
Memorizzare coppie chiave-valore con Preferences DataStore
L'implementazione di Preferences DataStore utilizza le classi DataStore
e
Preferences
per rendere persistenti le coppie chiave-valore su disco.
Crea un datastore delle preferenze
Utilizza il delegato della proprietà creato da preferencesDataStore
per creare un'istanza di DataStore<Preferences>
. Chiamalo una volta al livello superiore del tuo
file Kotlin e accedi tramite questa proprietà nel 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.
Kotlin
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
Java
RxDataStore<Preferences> dataStore =
new RxPreferenceDataStoreBuilder(context, "settings").build();
Leggere da un 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
Flow
.
Kotlin
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> =
context.dataStore.data.map { preferences ->
// No type safety.
preferences[EXAMPLE_COUNTER] ?: 0
}
Java
Preferences.Key<Integer> EXAMPLE_COUNTER =
PreferencesKeys.int("example_counter");
Flowable<Integer> exampleCounterFlow =
dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));
Scrivere in un Preferences DataStore
Preferences DataStore fornisce una funzione edit()
che aggiorna in modo transazionale
i dati in un DataStore
. Il parametro transform
della funzione accetta
un blocco di codice in cui puoi aggiornare i valori in base alle esigenze. Tutto il codice nel blocco
transform viene trattato come una singola transazione.
Kotlin
suspend fun incrementCounter() {
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
Java
Single<Preferences> updateResult = dataStore.updateDataAsync(prefsIn -> {
MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
Integer currentInt = prefsIn.get(INTEGER_KEY);
mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.
Archiviare oggetti tipizzati con Proto DataStore
L'implementazione di Proto DataStore utilizza DataStore e protocol buffer per salvare gli oggetti tipizzati su disco.
Definisci uno schema
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.
syntax = "proto3";
option java_package = "com.example.application.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Crea un datastore Proto
La creazione di un Proto DataStore per archiviare gli oggetti digitati richiede due passaggi:
- Definisci una classe che implementa
Serializer<T>
, doveT
è il tipo definito nel file proto. Questa classe di serializzazione 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. - Utilizza il delegato della proprietà creato da
dataStore
per creare un'istanza diDataStore<T>
, doveT
è il tipo definito nel file proto. Chiama questo una volta al livello superiore del file Kotlin e accedi tramite questo delegato di proprietà nel resto dell'app. Il parametrofilename
indica a DataStore quale file utilizzare per archiviare i dati, mentre il parametroserializer
specifica il nome della classe del serializzatore definita nel passaggio 1.
Kotlin
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) = t.writeTo(output)
}
val Context.settingsDataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer
)
Java
private static class SettingsSerializer implements Serializer<Settings> {
@Override
public Settings getDefaultValue() {
return Settings.getDefaultInstance();
}
@Override
public Settings readFrom(@NotNull InputStream input) {
try {
return Settings.parseFrom(input);
} catch (InvalidProtocolBufferException exception) {
throw CorruptionException("Cannot read proto.", exception);
}
}
@Override
public void writeTo(Settings t, @NotNull OutputStream output) {
t.writeTo(output);
}
}
RxDataStore<Byte> dataStore =
new RxDataStoreBuilder<Byte>(
context,
/* fileName= */ "settings.pb",
new SettingsSerializer()
).build();
Lettura da un archivio dati Proto
Utilizza DataStore.data
per esporre un Flow
della proprietà appropriata dall'oggetto
memorizzato.
Kotlin
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
.map { settings ->
// The exampleCounter property is generated from the proto schema.
settings.exampleCounter
}
Java
Flowable<Integer> exampleCounterFlow =
dataStore.data().map(settings -> settings.getExampleCounter());
Scrivere in un Proto DataStore
Proto 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 atomica di lettura-scrittura-modifica.
Kotlin
suspend fun incrementCounter() {
context.settingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setExampleCounter(currentSettings.exampleCounter + 1)
.build()
}
}
Java
Single<Settings> updateResult =
dataStore.updateDataAsync(currentSettings ->
Single.just(
currentSettings.toBuilder()
.setExampleCounter(currentSettings.getExampleCounter() + 1)
.build()));
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 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.
Considera un'applicazione di esempio con un servizio e un'attività:
Il servizio viene eseguito in un processo separato e aggiorna periodicamente DataStore
<service android:name=".MyService" android:process=":my_process_id" />
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { scope.launch { while(isActive) { dataStore.updateData { Settings(lastUpdate = System.currentTimeMillis()) } delay(1000) } } }
mentre l'app raccoglie queste modifiche e aggiorna la sua UI.
val settings: Settings by dataStore.data.collectAsState() Text( text = "Last updated: $${settings.timestamp}", )
Per poter utilizzare DataStore in processi diversi, devi creare
l'oggetto DataStore utilizzando MultiProcessDataStoreFactory
.
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
serializer
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:
@Serializable
data class Settings(
val lastUpdate: Long
)
@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {
override val defaultValue = Settings(lastUpdate = 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()
)
}
}
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 ripristino 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