DataStore Parte di Android Jetpack.
Jetpack DataStore è una soluzione di archiviazione dati che consente di archiviare coppie chiave-valore o oggetti digitati con buffer di protocollo. DataStore utilizza le coralli Kotlin e Flow per archiviare i dati in modo asincrono, coerente e transazionale.
Se al momento utilizzi SharedPreferences
per archiviare i dati, valuta la possibilità di eseguire la migrazione a DataStore.
Preferenze DataStore e Proto DataStore
DataStore offre due diverse implementazioni: Preferenze DataStore e Proto DataStore.
- Preferences DataStore archivia e accede ai dati utilizzando le chiavi. Questa implementazione non richiede uno schema predefinito e non offre sicurezza del tipo.
- Proto DataStore archivia i dati come istanze di un tipo di dati personalizzato. Questa implementazione richiede di definire uno schema utilizzando buffer di protocollo, ma offre sicurezza del tipo.
Utilizzo corretto del DataStore
Per utilizzare correttamente il datastore, ricorda sempre le seguenti regole:
Non creare mai più di un'istanza di
DataStore
per un determinato file nello stesso processo. Questo potrebbe interrompere tutte le funzionalità del datastore. Se nello stesso processo sono presenti più datastore attivi per un determinato file, DataStore generaIllegalStateException
durante la lettura o l'aggiornamento dei dati.Il tipo generico di DataStore
deve essere immutabile. La disattivazione di un tipo utilizzato in DataStore invalida qualsiasi garanzia fornita da DataStore e crea bug potenzialmente gravi e difficilmente raggiungibili. È vivamente consigliato utilizzare buffer di protocollo che garantiscano immutabilità, un'API semplice e una serializzazione efficiente.Non combinare mai gli utilizzi di
SingleProcessDataStore
eMultiProcessDataStore
per lo stesso file. Se intendi accedere aDataStore
da più di un processo, utilizza sempreMultiProcessDataStore
.
Configurazione
Per utilizzare Jetpack DataStore nella tua app, aggiungi quanto segue al file Gradle in base all'implementazione che vuoi utilizzare:
Preferenze di Datastore
Trendy
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.0.0" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.0.0" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.0.0") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.0.0") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.0.0") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.0.0") }
Datastore di protocollo
Trendy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.0.0" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.0.0" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.0.0" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.0.0" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.0.0") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.0.0") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.0.0") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.0.0") }
Memorizzazione di coppie chiave-valore con DataStore Preferenze
L'implementazione di Preferenze di datastore utilizza le classi DataStore
e Preferences
per salvare le coppie chiave-valore semplici 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 file kotlin e accedilo tramite questa proprietà per il resto della tua applicazione. In questo modo è più semplice mantenere il metodo 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, /*name=*/ "settings").build();
Leggere da un datastore delle preferenze
Poiché DataStore Preferenze non utilizza uno schema predefinito, devi utilizzare la funzione di tipo di chiave corrispondente per definire una chiave per ogni valore da 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 archiviato 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));
Scrivi in un datastore delle preferenze
Preferenze del datastore fornisce una funzione edit()
che aggiorna transativamente 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 di trasformazione viene considerato come un'unica 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.
Archivia oggetti digitati con Proto DataStore
L'implementazione di Proto DataStore utilizza DataStore e buffer di protocollo per rendere persistenti gli oggetti digitati 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
permanenti nel tuo Proto DataStore. Per scoprire di più su come definire uno schema di protocollo, consulta la guida al linguaggio protobuf.
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Crea un proto datastore
La creazione di Proto DataStore prevede due passaggi per l'archiviazione degli oggetti digitati:
- Definisci una classe che implementa
Serializer<T>
, doveT
è il tipo definito nel file proto. Questa classe 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. - Utilizza il delegato della proprietà creato da
dataStore
per creare un'istanza diDataStore<T>
, doveT
è il tipo definito nel file proto. Chiama il metodo una volta al livello superiore del file kotlin e accedi tramite questa proprietà. Delega nel resto dell'app. Il parametrofilename
indica a DataStore il file da utilizzare per archiviare i dati, mentre il parametroserializer
indica a DataStore il nome della classe 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() { Settings.getDefaultInstance(); } @Override public Settings readFrom(@NotNull InputStream input) { try { return Settings.parseFrom(input); } catch (exception: InvalidProtocolBufferException) { 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();
Leggi da Proto DataStore
Utilizza DataStore.data
per esporre un Flow
della proprietà appropriata dall'oggetto archiviato.
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());
Scrivi in un Proto DataStore
Proto DataStore fornisce una funzione updateData()
che aggiorna transativamente un oggetto archiviato. updateData()
fornisce lo stato attuale dei dati come un'istanza del tipo di dati e aggiorna i dati transativamente 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()));
Utilizza DataStore in codice sincrono
Uno dei principali vantaggi di DataStore è l'API asincrona, ma potrebbe non essere sempre possibile cambiare il codice circostante in modo asincrono. Questo potrebbe verificarsi se stai utilizzando un codebase esistente che utilizza l'I/O del disco sincrono o se hai una dipendenza che non fornisce un'API asincrona.
Le coroutine Kotlin forniscono il generatore di coroutine runBlocking()
per aiutare a colmare il divario tra il codice sincrono e asincrono. Puoi utilizzare runBlocking()
per leggere i dati di DataStore in modo sincrono.
RxJava offre metodi di blocco su Flowable
. Il seguente codice blocca il thread di chiamata finché DataStore non restituisce 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 dell'interfaccia utente può causare blocchi degli ANR o della UI. 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, il datastore legge in modo asincrono i dati e li memorizza nella cache. Le letture sincrone successive che utilizzano runBlocking()
potrebbero essere più veloci o potrebbero evitare del tutto un'operazione di I/O del disco se la lettura iniziale è stata completata.
Utilizza DataStore in codice multiprocesso
Puoi configurare DataStore in modo che acceda agli stessi dati in più processi con le stesse garanzie di coerenza dei dati di un singolo processo. In particolare, DataStore garantisce:
- La lettura restituisce solo i dati conservati in modo permanente sul disco.
- Coerenza tra lettura e scrittura.
- Le scritture sono serializzate.
- Le letture non sono mai bloccate dalle scritture.
Considera un'applicazione di esempio con un servizio e un'attività:
Il servizio è in esecuzione in un processo separato e aggiorna periodicamente il 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 diversi processi, 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 utilizzando 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): Timer =
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'iniezione di dipendenze Hilt per assicurarti che l'istanza di DataStore sia univoca per processo:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
Fornire feedback
Condividi un feedback e idee tramite queste risorse:
- Issue Tracker
- Segnala i problemi per consentirci di risolverli.
Risorse aggiuntive
Per scoprire di più su Jetpack DataStore, consulta le seguenti risorse aggiuntive:
Samples
Blog
codelab
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript non è attivo
- Caricare e visualizzare i dati impaginati
- Panoramica di LiveData
- Layout ed espressioni di associazione