DataStore è un componente della libreria Jetpack progettato per archiviare piccole quantità di dati nelle applicazioni Android. A differenza di SharedPreferences, funziona in modo asincrono e garantisce la coerenza dei dati in caso di accesso concorrente. Secondo Google, 2024, DataStore utilizza Kotlin Coroutines e Flow, rendendolo sicuro per il thread principale e adatto per architetture reattive.
Punti Chiave
DataStore è una soluzione di Google per l'archiviazione locale dei dati in Android, presentata nel 2020 come alternativa a SharedPreferences. Supporta due modalità: Preferences DataStore (semplici coppie chiave-valore) e Proto DataStore (schema tipizzato basato su Protocol Buffers).
Il vantaggio principale è la completa asincronia: tutte le operazioni di lettura restituiscono un Flow da Kotlin Coroutines e le scritture vengono eseguite in un contesto di coroutine. Ciò elimina il blocco del thread principale, che era un problema tipico di SharedPreferences nella gestione di grandi volumi di dati.
DataStore garantisce l'atomicità delle operazioni: le scritture concorrenti non causano perdita di dati grazie al suo modello transazionale. Se due componenti modificano lo stesso valore simultaneamente, DataStore gestisce correttamente il conflitto tramite un meccanismo compare-and-swap.
Secondo Google I/O 2023, DataStore è utilizzato nel 40% dei nuovi progetti Android e Google raccomanda di migrare da SharedPreferences in tutte le applicazioni dove è richiesta stabilità nell'archiviazione delle impostazioni.
Al centro di DataStore c'è SingleProcessDataStore — un'implementazione che opera all'interno di un singolo processo. Utilizza un archivio basato su file con blocco a livello di file: durante la scrittura dei dati, il file viene bloccato, prevenendo la corruzione in caso di accesso concorrente.
DataStore gestisce automaticamente gli errori di deserializzazione: se il file è danneggiato, restituisce un valore predefinito e sovrascrive il file. Questo comportamento è configurabile tramite corruptionHandler, che può essere impostato durante la creazione di DataStore.
SharedPreferences soffre di tre problemi fondamentali: lettura sincrona dal disco sul thread principale, mancanza di garanzie di atomicità per scritture concorrenti e incapacità di tracciare i cambiamenti in modo reattivo. DataStore risolve tutti e tre: Flow per l'osservazione, blocco dei file per l'atomicità e API asincrona per la sicurezza dei thread.
DataStore memorizza i dati in file nella memoria interna del dispositivo. Preferences DataStore utilizza un formato di file simile a SharedPreferences ma con metadati aggiuntivi per la verifica dell'integrità. Proto DataStore utilizza il formato binario Protocol Buffers, che riduce le dimensioni del file e accelera la serializzazione.
Durante la lettura dei dati, DataStore carica l'intero file in memoria una volta, dopo di che gli abbonati ricevono lo stato corrente tramite Flow. Le modifiche vengono trasmesse automaticamente a tutti gli abbonati attivi — non è richiesta la registrazione manuale di listener, come in SharedPreferences.
Preferences DataStore utilizza un meccanismo di serializzazione integrato basato su una Map. Ogni voce è una coppia di una stringa e un tipo primitivo (Int, Boolean, Float, Long, String, Set). I dati sono memorizzati in un file XML, simile a SharedPreferences, ma con scrittura atomica tramite blocco del file.
Esempio di creazione di Preferences DataStore: l'estensione preferencesDataStore su Context crea un singleton con il nome del file. A chiamate ripetute, viene restituita la stessa istanza — ciò elimina la duplicazione dei file e la confusione tra diverse istanze di archiviazione.
Proto DataStore richiede la definizione di uno schema dati tramite un file .proto e la compilazione con il plugin protobuf. La classe Java generata viene utilizzata come punto di ingresso unico per tutti i campi — ciò elimina gli errori di battitura nelle chiavi, comuni con SharedPreferences.
Lo schema di Proto DataStore viene definito una volta e supporta l'aggiunta di nuovi campi senza perdere i vecchi dati. Se una nuova versione dell'app aggiunge un campo con un valore predefinito, il vecchio file verrà deserializzato correttamente — la compatibilità all'indietro è integrata nel protocollo.
La scelta tra Preferences DataStore e Proto DataStore dipende dalla complessità dei dati e dai requisiti di tipizzazione. Entrambe le opzioni sono asincrone e transazionali, ma differiscono per sicurezza dei tipi e prestazioni di serializzazione.
| Caratteristica | Preferences DataStore | Proto DataStore |
|---|---|---|
| Tipizzazione | Debole (chiave-valore) | Forte (classe generata) |
| Serializzazione | XML (integrata) | Protocol Buffers (protobuf) |
| Dimensione file | Grande (XML leggibile) | Piccola (binaria) |
| Complessità | Bassa (senza .proto) | Media (richiede .proto) |
| Migrazione schema | Nessuno schema | Automatica (proto) |
| Compatibilità | SharedPreferences (tramite migrazione) | Solo Proto DataStore |
Preferences DataStore è adatto per impostazioni semplici: flag di funzionalità, stringa del token di autorizzazione, conteggio avvii dell'app. Se i dati sono pochi (fino a 10–15 chiavi) e non richiedono uno schema rigoroso, Preferences DataStore fornisce una soglia di ingresso minima senza collegare il plugin protobuf.
Proto DataStore è giustificato quando la struttura dei dati è complessa o può cambiare tra le versioni dell'app. Ad esempio, impostazioni del profilo utente o configurazione di test A/B con 20+ campi. Protobuf fornisce una tipizzazione forte e migrazioni automatiche, eliminando errori di runtime dovuti a discrepanze di chiavi.
Google fornisce un meccanismo di migrazione integrato tramite la classe SharedPreferencesMigration. La migrazione viene eseguita una volta al primo avvio dopo l'aggiornamento dell'app: DataStore legge i dati da SharedPreferences, li scrive nel proprio formato e contrassegna la migrazione come completata.
La migrazione supporta trasformazioni personalizzate: se le chiavi in SharedPreferences non corrispondono alle chiavi desiderate di DataStore, è possibile specificare una funzione di trasformazione tramite SharedPreferencesMigration. Ciò consente di rinominare le chiavi e cambiare i tipi di dati durante la migrazione.
Primo, aggiungi DataStore a build.gradle e crea un'istanza di DataStore con migrazione: SharedPreferencesMigration accetta il nome del file SharedPreferences e un insieme di chiavi da trasferire. Secondo, rimuovi tutto il codice che funziona tramite SharedPreferences e sostituiscilo con chiamate a DataStore. Terzo, testa la migrazione: al primo avvio, i dati dovrebbero apparire in DataStore e il vecchio file SharedPreferences non dovrebbe più essere utilizzato.
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(context, "old_prefs")
)
}
)
DataStore si integra facilmente in un progetto esistente. Di seguito sono riportati esempi pratici per Preferences DataStore e Proto DataStore — entrambi dimostrano la lettura, la scrittura e l'osservazione reattiva dei dati.
In questo esempio, Preferences DataStore memorizza tre impostazioni: tema scuro, nome utente e conteggio avvii. La lettura avviene tramite l'estensione .data, che restituisce un Flow. La scrittura avviene tramite la funzione suspend .edit, che garantisce l'atomicità delle modifiche.
val Context.settingsDataStore by preferencesDataStore(name = "settings")
val isDarkMode: Flow<Boolean> = settingsDataStore.data
.map { preferences ->
preferences[booleanPreferencesKey("dark_mode")] ?: false
}
suspend fun toggleDarkMode() {
settingsDataStore.edit { prefs ->
val current = prefs[booleanPreferencesKey("dark_mode")] ?: false
prefs[booleanPreferencesKey("dark_mode")] = !current
}
}
Proto DataStore richiede la definizione di un file .proto. Dopo la compilazione, viene creata una classe UserSettings utilizzata per lettura e scrittura. Le migrazioni di versione dello schema sono descritte nello stesso file .proto e vengono applicate automaticamente.
// user_preferences.proto
syntax = "proto3";
message UserPreferences {
string display_name = 1;
int32 notification_count = 2;
bool notifications_enabled = 3;
}
// Lettura da DataStore
val userPreferencesFlow: Flow<UserPreferences> =
protoDataStore.data
// Scrittura di nuovi valori
suspend fun updateDisplayName(name: String) {
protoDataStore.updateData { prefs ->
prefs.toBuilder()
.setDisplayName(name)
.build()
}
}
DataStore si integra con l'architettura MVVM tramite ViewModel. Il Flow da DataStore viene raccolto tramite .stateIn e utilizzato nell'interfaccia utente. A ogni modifica dei dati, l'interfaccia utente si aggiorna automaticamente — non sono necessari aggiornamenti manuali o LiveData.
class SettingsViewModel(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
val uiState: StateFlow<SettingsUiState> =
dataStore.data
.map { prefs ->
SettingsUiState(
isDarkMode = prefs[booleanPreferencesKey("dark_mode")] ?: false,
counter = prefs[intPreferencesKey("launch_count")] ?: 0
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = SettingsUiState()
)
}
Domande Frequenti
DataStore funziona in modo asincrono (non blocca il thread UI), supporta l'accesso concorrente tramite transazioni e consente l'abbonamento reattivo alle modifiche tramite Flow. SharedPreferences è un'API sincrona con rischio di ANR con grandi volumi di dati e senza supporto reattivo integrato.
DataStore è scritto in Kotlin e richiede Kotlin Coroutines. Usarlo da Java è possibile ma scomodo: si dovrebbero creare wrapper con CompletableFuture o gestire manualmente le coroutine. Per progetti Java, Google raccomanda di mantenere SharedPreferences o aggiungere Kotlin al modulo.
DataStore carica l'intero file in memoria durante la lettura, quindi non è adatto per archiviare elenchi o oggetti grandi. Per tali scenari, usa Room o SQLite. DataStore è ottimizzato per impostazioni e piccoli dati strutturati — fino a centinaia di kilobyte.
Durante la creazione di DataStore, puoi passare un corruptionHandler — una funzione chiamata quando il file è danneggiato. Per impostazione predefinita, DataStore lancia un'eccezione CorruptionException. Nel corruptionHandler, puoi restituire dati vuoti, dopodiché DataStore sovrascriverà il file con uno stato corretto.
Sì, Proto DataStore richiede la definizione di uno schema in un file .proto e la connessione del protobuf-gradle-plugin. Se il progetto è piccolo e i dati sono semplici, è più facile usare Preferences DataStore — non richiede configurazione di build aggiuntiva.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche