DataStore   Componente di Android Jetpack.

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

Se al momento utilizzi SharedPreferences per memorizzare i dati, ti consigliamo di eseguire la migrazione a DataStore.

Preferences DataStore e Proto DataStore

DataStore fornisce due implementazioni diverse: Preferences DataStore e Proto DataStore.

  • Preferences DataStore memorizza e accede ai dati utilizzando le chiavi. Questa implementazione non richiede uno schema predefinito e non garantisce la sicurezza del tipo.
  • 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 garantisce la sicurezza del tipo.

Utilizzare correttamente DataStore

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

  1. Non creare mai più di un'istanza di DataStore per un determinato file nella stessa procedura. In questo modo, è possibile interrompere tutte le funzionalità di DataStore. Se sono attivi più DataStore per un determinato file nella stessa procedura, DataStore genererà un IllegalStateException durante la lettura o l'aggiornamento dei dati.

  2. Il tipo generico del DataStore deve essere immutabile. La mutazione di un tipo usato in DataStore invalida le garanzie fornite da DataStore e crea bug potenzialmente gravi e difficili da rilevare. Ti consigliamo vivamente di utilizzare i buffer di protocollo che forniscono garanzie di immutabilità, un'API semplice e una serializzazione efficiente.

  3. Non combinare mai l'utilizzo di SingleProcessDataStore e MultiProcessDataStore per lo stesso file. Se intendi accedere a DataStore da più di un processo, utilizza sempre MultiProcessDataStore.

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.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.1"
    }
    

Kotlin

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.1")
    }
    

Proto DataStore

Groovy

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.1"
    }
    

Kotlin

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.1")
    }
    

Memorizzare coppie chiave/valore con Preferences DataStore

L'implementazione di Datastore delle preferenze utilizza le classi DataStore e Preferences per mantenere su disco coppie chiave-valore semplici.

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 accedivi tramite questa proprietà nel resto dell'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 Preferences.

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

Lettura da un DataStore delle preferenze

Poiché Preferences DataStore non utilizza uno schema predefinito, devi utilizzare la funzione di tipo di chiave corrispondente per definire una chiave per ogni valore da memorizzare 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 DataStore delle preferenze

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 di trasformazione viene considerato 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.

Memorizza oggetti con tipi con Proto DataStore

L'implementazione di Proto DataStore utilizza DataStore e buffer di protocollo per mantenere permanenti gli oggetti di tipo sul disco.

Definire uno schema

Proto DataStore richiede uno schema predefinito in un file proto nella directoryapp/src/main/proto/. Questo schema definisce il tipo di oggetti che persisti 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";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Crea un datastore Proto

Per creare un Proto DataStore per archiviare gli oggetti di tipo, sono necessari due passaggi:

  1. Definisci una classe che implementa Serializer<T>, dove T è 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 da utilizzare per il serializzatore se non è stato ancora creato alcun file.
  2. Utilizza il delegato della proprietà creato da dataStore per creare un'istanza di DataStore<T>, dove T è il tipo definito nel file proto. Chiama questa funzione una volta al livello superiore del file Kotlin e accedivi tramite questa proprietà delegata nel resto dell'app. Il parametro filename indica a DataStore quale file utilizzare per archiviare i dati e il parametro serializer indica a DataStore il nome della classe di serializzazione 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();

Lettura da un 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());

Scrivere in un Proto DataStore

Proto DataStore fornisce una funzione updateData() che aggiorna in modo transazionale un oggetto archiviato. updateData() ti fornisce lo stato corrente dei dati come istanza del tipo di dati e aggiorna i dati in modo transazionale in un'operazione atomica di lettura, scrittura e 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 accadere se utilizzi una base di codice esistente che utilizza I/O disco sincrono o se hai una dipendenza che non fornisce un'API asincrona.

Le coroutine Kotlin forniscono il compilatore di coroutine runBlocking() per colmare il divario tra il codice sincrono e asincrono. Puoi utilizzare runBlocking() per leggere i dati da 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 I/O sincrone sul thread dell'interfaccia utente può causare ANR o balbuzie dell'interfaccia utente. 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() possono 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 in modo che acceda agli stessi dati in diversi processi con le stesse garanzie di coerenza dei dati di un singolo processo. In particolare, DataStore garantisce:

  • Le letture restituiscono solo i dati che sono stati resi permanenti sul disco.
  • Coerenza read-after-write.
  • Le scritture vengono serializzate.
  • Le letture non vengono mai bloccate dalle scritture.

Prendiamo in considerazione un'applicazione di esempio con un servizio e un'attività:

  1. 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)
              }
          }
    }
    
  2. L'app raccoglie queste modifiche e aggiorna la sua interfaccia utente

    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 è stato ancora 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): 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 dipendenza Hilt per assicurarti che l'istanza DataStore sia univoca per processo:

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

Gestire i file danneggiati

In rari casi, il file su disco persistente di DataStore potrebbe essere danneggiato. Per impostazione predefinita, DataStore non si riprende automaticamente dalla corruzione e i tentativi di lettura causeranno l'emissione di un CorruptionException da parte del sistema.

DataStore offre un'API di gestione dei danneggiamenti che può aiutarti a eseguire un recupero ordinato in questo tipo di scenario ed evitare di generare l'eccezione. Se configurato, il gestore delle alterazioni sostituisce il file danneggiato con uno nuovo contenente un valore predefinito.

Per configurare questo gestore, fornisci un corruptionHandler durante la creazione dell'istanza DataStore in by dataStore() o nel metodo di fabbrica 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 i tuoi feedback e le tue idee tramite queste risorse:

Issue Tracker
Segnala i problemi per consentirci di correggere i bug.

Risorse aggiuntive

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

Campioni

Blog

Codelab