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

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

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

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

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

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

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

Proto DataStore

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

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

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

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

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

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

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

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";
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): 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()
       
)
   
}
}

É 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 da 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

Nenhum resultado foi encontrado.

Blogs

Codelabs

Nenhuma recomendação no momento.

Tente na sua Conta do Google.