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 tipos.

Como usar o DataStore da forma correta

Para usar o DataStore da forma correta, siga estas regras:

  1. Nunca crie mais de uma instância do DataStore para um determinado arquivo no mesmo processo. Essa ação pode interromper toda a funcionalidade do DataStore. Se houver vários DataStores ativos para um determinado arquivo no mesmo processo, o DataStore vai gerar IllegalStateException ao ler ou atualizar dados.

  2. O tipo genérico do DataStore precisa ser imutável. A mutação de um tipo usado no DataStore invalida todas as garantias fornecidas e cria bugs potencialmente graves e difíceis de detectar. É altamente recomendável usar buffers de protocolo que ofereçam garantias de imutabilidade, uma API simples e uma serialização eficiente.

  3. Nunca misture os usos do SingleProcessDataStore e do MultiProcessDataStore no mesmo arquivo. Se você pretende acessar o DataStore em mais de um processo, sempre use MultiProcessDataStore.

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

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

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

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

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.5"
    }
    
    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.5")

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

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

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

Proto DataStore

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

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

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

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.5"
    }
    
    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.5")

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

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

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

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.

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
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.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}
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.

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}
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.proto";
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.
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
)
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.

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }
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.

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}
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:

val exampleData = runBlocking { context.dataStore.data.first() }
Settings settings = dataStore.data().blockingFirst();

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

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}
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.

Usar o DataStore em códigos com vários processos

É possível configurar o DataStore para acessar os mesmos dados em processos diferentes e com as mesmas garantias de consistência presentes em um único processo. Especificamente, o DataStore garante que:

  • As leituras retornem apenas os dados mantidos no disco.
  • As leituras após gravações sejam consistentes.
  • As gravações sejam serializadas.
  • As leituras nunca sejam bloqueadas por gravações.

Considere um aplicativo de exemplo com um serviço e uma atividade:

  1. O serviço está sendo executado em um processo separado e atualiza periodicamente o 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. Já o app coleta essas mudanças e atualiza a interface.

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

Para usar o DataStore em diferentes processos, você precisa criar o objeto dele usando a MultiProcessDataStoreFactory.

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)

O serializer informa ao DataStore como ler e gravar o tipo de dados. Inclua um valor padrão para o serializador que será usado se ainda não houver arquivos criados. Consulte abaixo um exemplo de implementação usando a 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): Settings =
       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()
       )
   }
}

É possível usar a injeção de dependências de Hilt para garantir que a instância do DataStore seja única para cada processo:

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

Processar a corrupção de arquivos

Em raras ocasiões, o arquivo persistente do DataStore no disco pode ficar corrompido. Por padrão, o DataStore não se recupera automaticamente de corrupção, e as tentativas de leitura dele fazem com que o sistema gere uma CorruptionException.

O DataStore oferece uma API de gerenciador de corrupção que pode ajudar você a se recuperar nesse cenário e evitar a exceção. Quando configurado, o gerenciador de corrupção substitui o arquivo corrompido por um novo que contém um valor padrão predefinido.

Para configurar esse manipulador, forneça um corruptionHandler ao criar a instância do DataStore em by dataStore() ou no método de fábrica DataStoreFactory:

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

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:

Exemplos

Learn how this app was designed and built in the design case study, architecture learning journey and modularization learning journey. This is the repository for the Now in Android app. It is a work in progress 🚧. Now in Android is a fully functional

Blogs

Codelabs

Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.

Atualização: Dec 22, 2024

Use o LiveData para lidar com dados considerando o ciclo de vida.

Atualização: Dec 22, 2024

Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.

Atualização: Sep 20, 2024