DataStore   Parte do Android Jetpack.

O Jetpack DataStore é uma solução de armazenamento de dados que permite armazenar pares de chave-valor ou objetos tipados com buffers de protocolo. O DataStore usa corrotinas e fluxo do Kotlin para armazenar dados de forma assíncrona, consistente e transacional.

Se você estiver usando SharedPreferences para armazenar dados, considere migrar para o DataStore.

Preferences DataStore e Proto DataStore

O DataStore oferece duas implementações diferentes: Preferences DataStore e Proto DataStore.

  • O Preferences DataStore armazena e acessa dados usando chaves. Essa implementação não requer um esquema predefinido e não fornece segurança de tipo.
  • O Proto DataStore armazena dados como instâncias de um tipo de dados personalizado. Essa implementação requer a definição de um esquema usando buffers de protocolo, mas fornece segurança de tipo.

Configurar

Para usar o Jetpack DataStore no seu app, adicione o seguinte ao arquivo Gradle, dependendo da implementação que você quer usar:

Preferences DataStore

Groovy

    // 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")
    }
    

Proto DataStore

Groovy

    // 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")
    }
    

Armazenar pares de chave-valor com Preferences DataStore

A implementação Preferences DataStore usa as classes DataStore e Preferences para manter pares simples de chave-valor no disco.

Criar um Preferences DataStore

Use a delegação da propriedade criada por preferencesDataStore para criar uma instância de Datastore<Preferences>. Faça a chamada uma vez no nível superior do arquivo Kotlin e acesse-a usando essa propriedade em todo o restante do aplicativo. Isso facilita a manutenção de DataStore como um Singleton. Como alternativa, use RxPreferenceDataStoreBuilder se estiver usando RxJava. O parâmetro name obrigatório é o nome do Preferences DataStore.

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

Ler em um Preferences DataStore

Como o Preferences DataStore não usa um esquema predefinido, é necessário usar a função de tipo de chave correspondente para definir uma chave para cada valor que precisa armazenar na instância DataStore<Preferences>. Por exemplo, para definir uma chave para um valor de int, use intPreferencesKey(). Em seguida, use a propriedade DataStore.data para expor o valor armazenado apropriado usando um 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));

Gravar em um Preferences DataStore

O Preferences DataStore disponibiliza uma função edit() que atualiza os dados de forma transacional em um DataStore. O parâmetro transform da função aceita um bloco de código em que você pode atualizar os valores conforme necessário. Todo o código no bloco de transformação é tratado como uma única transação.

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.

Armazenar objetos tipados com Proto DataStore

A implementação Proto DataStore usa DataStore e buffers de protocolo para manter objetos tipados no disco.

Definir um esquema

O Proto DataStore requer um esquema predefinido em um arquivo proto no diretório app/src/main/proto/. Esse esquema define o tipo dos objetos que você persiste no Proto DataStore. Para saber mais sobre como definir um esquema proto, consulte o guia de linguagem do protobuf (em inglês).

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Criar um Proto DataStore

Há duas etapas envolvidas na criação de um Proto DataStore para armazenar os objetos tipados:

  1. Defina uma classe que implemente Serializer<T>, em que T é o tipo definido no arquivo proto. Essa classe de serializador informa ao DataStore como ler e gravar o tipo de dados. Inclua um valor padrão para o serializador a ser usado se ainda não houver arquivos criados.
  2. Use a delegação de propriedade criada por dataStore para criar uma instância de DataStore<T> em que T é o tipo definido no arquivo proto. Faça a chamada uma vez no nível superior do arquivo Kotlin e acesse-a usando a delegação de propriedade em todo o restante do app. O parâmetro filename informa ao DataStore qual arquivo usar para armazenar os dados, e o parâmetro serializer informa ao DataStore o nome da classe do serializador definida na etapa 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();

Ler em um Proto DataStore

Use DataStore.data para expor um Flow da propriedade apropriada do seu objeto armazenado.

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

Gravar em um Proto DataStore

O Proto DataStore disponibiliza uma função updateData() que atualiza transacionalmente um objeto armazenado. updateData() fornece o estado atual dos dados como uma instância do seu tipo de dados e os atualiza de maneira transacional em uma operação atômica de leitura-gravação-modificação.

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

Usar o DataStore no código síncrono

Um dos principais benefícios do DataStore é a API assíncrona, mas nem sempre é possível mudar o código circundante para que ele seja assíncrono. Isso poderá acontecer se você estiver trabalhando com uma base de código existente que usa E/S de disco síncrono ou se você tiver uma dependência que não fornece uma API assíncrona.

As corrotinas do Kotlin fornecem o builder de corrotinas runBlocking() (link em inglês) para ajudar a preencher a lacuna entre o código síncrono e assíncrono. Você pode usar runBlocking() para ler dados do DataStore de forma síncrona. O RxJava oferece métodos de bloqueio em Flowable. O código a seguir bloqueia a linha de execução de chamada até que o DataStore retorne dados:

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

A execução de operações síncronas de E/S na linha de execução de IU pode causar ANRs ou instabilidade da IU. É possível atenuar esses problemas carregando antecipadamente os dados do DataStore:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

Dessa forma, o DataStore lê de maneira assíncrona os dados e os armazena em cache na memória. As leituras síncronas posteriores que usam runBlocking() podem ser mais rápidas ou podem evitar completamente uma operação de E/S de disco caso a leitura inicial seja concluída.

Enviar feedback

Envie comentários e ideias usando os recursos abaixo:

Issue tracker
Informe os problemas para que possamos corrigir os bugs.

Outros recursos

Para saber mais sobre o Jetpack DataStore, consulte os seguintes recursos extras:

Blogs

Codelabs