Datastore Componente 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 Coroutines 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 di Datastore e Proto DataStore

DataStore fornisce due diverse implementazioni: Preferences (Preferenze) e Proto DataStore.

  • Preferenze DataStore archivia e accede ai dati utilizzando le chiavi. Questa implementazione non richiede uno schema predefinito e non offre sicurezza del tipo.
  • Il datastore Proto archivia i dati come istanze di un tipo di dati personalizzato. Questa implementazione richiede la definizione di uno schema utilizzando i buffer di protocollo, ma offre sicurezza per i tipi.

Utilizzo corretto di DataStore

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

  1. Non creare mai più di un'istanza di DataStore per un determinato file nello stesso processo. altrimenti potrebbero interrompere tutte le funzionalità di DataStore. Se ci sono più datastore attivi per un determinato file nello stesso processo, DataStore genera IllegalStateException durante la lettura o l'aggiornamento dei dati.

  2. Il tipo generico di DataStore deve essere immutabile. La modifica di un tipo utilizzato in DataStore invalida qualsiasi garanzia fornita da DataStore e crea bug potenzialmente gravi e difficili da recuperare. Ti consigliamo vivamente di utilizzare buffer di protocollo che offrono garanzie di immutabilità, un'API semplice e una serializzazione efficiente.

  3. Non combinare mai gli utilizzi di SingleProcessDataStore e MultiProcessDataStore per lo stesso file. Se intendi accedere a DataStore da più processi, utilizza sempre MultiProcessDataStore.

Configurazione

Per utilizzare Jetpack DataStore nella tua app, aggiungi quanto segue al file Gradle a seconda dell'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")
    }
    

Memorizzare coppie chiave-valore con il Datastore delle preferenze

L'implementazione Preferences DataStore utilizza le classi DataStore e Preferences per mantenere semplici coppie chiave-valore sul disco.

Crea un datastore preferenze

Utilizza il delegato della proprietà creato da preferencesDataStore per creare un'istanza di Datastore<Preferences>. Richiamalo una volta al livello superiore del tuo file kotlin e accedivi tramite questa proprietà per il resto dell'applicazione. In questo modo è più facile conservare DataStore come un singolo esemplare. In alternativa, utilizza RxPreferenceDataStoreBuilder se usi 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();

Lettura da un datastore delle preferenze

Poiché preferenze DataStore non utilizza uno schema predefinito, devi utilizzare la funzione del tipo di chiave corrispondente per definire una chiave per ciascun 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

Preferred 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 secondo necessità. Tutto il codice nel blocco della trasformazione 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.

Archivia oggetti digitati con Proto DataStore

L'implementazione di Proto DataStore utilizza DataStore e i buffer di protocollo per mantenere gli oggetti digitati su disco.

Definisci uno schema

Proto DataStore richiede uno schema predefinito in un file di protocollo nella directory app/src/main/proto/. Questo schema definisce il tipo degli oggetti che vengono mantenuti nel datastore di Proto. 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

La creazione di un DataStore Proto in cui archiviare gli oggetti digitati prevede due passaggi:

  1. Definisci una classe che implementa Serializer<T>, dove T è il tipo definito nel file di protocollo. Questa classe di 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.
  2. Utilizza il delegato della proprietà creato da dataStore per creare un'istanza di DataStore<T>, dove T è il tipo definito nel file di protocollo. Chiamalo una volta al livello superiore del file kotlin e accedi tramite questa proprietà delega 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.

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 datastore Proto

Utilizza DataStore.data per esporre 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 datastore Proto

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 li aggiorna in modo transazionale in un'operazione di lettura, scrittura e modifica atomica.

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 modificare il codice circostante in modo che sia asincrono. ad esempio se stai lavorando con 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 codice sincrono e asincrono. Puoi utilizzare runBlocking() per leggere i dati dal datastore in modo sincrono. RxJava offre metodi di blocco su Flowable. Il seguente codice blocca il thread di chiamata fino a quando DataStore 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 della UI può causare errori ANR o interruzioni dell'UI. Puoi mitigare questi problemi precaricando in modo asincrono i dati dal 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. Le letture sincrone successive utilizzando runBlocking() potrebbero essere più veloci o potrebbero evitare del tutto un'operazione di I/O sul disco se la lettura iniziale è stata completata.

Utilizza DataStore nel codice multi-processo

Puoi configurare DataStore in modo da accedere agli stessi dati in diversi processi con le stesse garanzie di coerenza dei dati disponibili all'interno di un singolo processo. In particolare, DataStore garantisce:

  • Le letture restituiscono solo i dati resi persistenti su disco.
  • Coerenza lettura-dopo-scrittura.
  • Le scritture sono serializzate.
  • Le operazioni di lettura non sono mai bloccate da operazioni di scrittura.

Considera 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. Anche se l'app raccoglierebbe queste modifiche e aggiornerà 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'inserimento della dipendenza Hilt per assicurarti che l'istanza DataStore sia univoca per processo:

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

Fornisci feedback

Condividi il tuo feedback e le tue idee con noi attraverso queste risorse:

Issue Tracker
Segnala i problemi per consentirci di correggerli.

Risorse aggiuntive

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

Samples

Blog

Codelab