DataStore   Parte do Android Jetpack.

Testar com o Kotlin Multiplatform
O Kotlin Multiplatform permite compartilhar a camada de dados com outras plataformas. Saiba como configurar e trabalhar com o DataStore no KMP

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

API DataStore

A DataStore interface fornece a seguinte API:

  1. Um fluxo que pode ser usado para ler dados do DataStore

    val data: Flow<T>
    
  2. Uma função para atualizar dados no DataStore

    suspend updateData(transform: suspend (t) -> T)
    

Configurações do DataStore

Se você quiser armazenar e acessar dados usando chaves, use a implementação do Preferences DataStore, que não exige um esquema predefinido e não oferece segurança de tipos. Ele tem uma API semelhante a SharedPreferences, mas não tem as desvantagens associadas a preferências compartilhadas.

O DataStore permite manter classes personalizadas. Para fazer isso, defina um esquema para os dados e forneça um Serializer para convertê-los em um formato persistente. Você pode usar buffers de protocolo, JSON ou qualquer outra estratégia de serialização.

Configuração

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

Preferences DataStore

Adicione as seguintes linhas à parte de dependências do arquivo gradle:

Groovy

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

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.2.1"
    }
    

Kotlin

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

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.2.1")
    }
    

Para adicionar suporte opcional ao RxJava, adicione as seguintes dependências:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.2.1")

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

DataStore

Adicione as seguintes linhas à parte de dependências do arquivo gradle:

Groovy

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.2.1"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.2.1"
    }
    

Kotlin

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.2.1")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.2.1")
    }
    

Adicione as seguintes dependências opcionais para compatibilidade com RxJava:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.2.1"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.2.1")

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

Para serializar conteúdo, adicione dependências para buffers de protocolo ou serialização JSON.

Serialização JSON

Para usar a serialização JSON, adicione o seguinte ao arquivo do Gradle:

Groovy

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Kotlin

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

Serialização do Protobuf

Para usar a serialização do Protobuf, adicione o seguinte ao arquivo do Gradle:

Groovy

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Kotlin

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

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<T> precisa ser imutável. A mutação de um tipo usado no DataStore invalida a consistência fornecida e cria bugs potencialmente graves e difíceis de detectar. Recomendamos o uso de buffers de protocolo, que ajudam a garantir a imutabilidade, uma API clara e uma serialização eficiente.

  3. Não misture usos de SingleProcessDataStore e MultiProcessDataStore para o mesmo arquivo. Se você pretende acessar o DataStore em mais de um processo, use MultiProcessDataStore.

Definição de dados

Preferences DataStore

Defina uma chave que será usada para manter os dados no disco.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

Para o repositório de dados JSON, adicione uma anotação @Serialization aos dados que você quer manter.

@Serializable
data class Settings(
    val exampleCounter: Int
)

Defina uma classe que implemente Serializer<T>, em que T é o tipo da classe a que você adicionou a anotação anterior. Inclua um valor padrão para o serializador a ser usado se ainda não houver arquivos criados.

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 0)

    override suspend fun readFrom(input: InputStream): Settings =
        try {
            Json.decodeFromString<Settings>(
                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(t)
                .encodeToByteArray()
        )
    }
}

Proto DataStore

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

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

Adicione um arquivo chamado settings.proto na pasta src/main/proto:

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Defina uma classe que implemente Serializer<T>, em que T é o tipo definido no arquivo proto. Essa classe de serializador define como o DataStore lê e grava o tipo de dados. Inclua um valor padrão para o serializador a ser usado se ainda não houver arquivos criados.

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) {
        return t.writeTo(output)
    }
}

Criar um DataStore

É necessário especificar um nome para o arquivo usado para manter os dados.

Preferences DataStore

A implementação Preferences DataStore usa as classes DataStore e Preferences para manter pares de chave-valor no disco. 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. Acesse o DataStore usando essa propriedade em todo o restante do aplicativo. Isso facilita a manutenção do DataStore como um Singleton. 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")

JSON DataStore

Use a delegação de propriedade criada por dataStore para criar uma instância de DataStore<T>, em que T é a classe de dados serializável. 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 anteriormente.

val Context.dataStore: DataStore<Settings> by dataStore(
    fileName = "settings.json",
    serializer = SettingsSerializer,
)

Proto DataStore

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

val Context.dataStore: DataStore<Settings> by dataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer,
)

Ler do DataStore

É necessário especificar um nome para o arquivo usado para manter os dados.

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 você precisa armazenar na instância DataStore<Preferences>. Por exemplo, para definir uma chave para um valor inteiro, use intPreferencesKey. Em seguida, use a DataStore.data propriedade para expor o valor armazenado apropriado usando um fluxo.

fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
    preferences[EXAMPLE_COUNTER] ?: 0
}

JSON DataStore

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Proto DataStore

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Use collectAsStateWithLifecycle para consumir o Flow produzido por um ViewModel em um elemento combinável. Isso converte com segurança o fluxo do DataStore em um estado do Compose que aciona a recomposição.

@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
  val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
  Text(text = "Example counter: ${counter}")
}

Para mais informações sobre collectAsStateWithLifecycle, consulte Estado e Jetpack Compose.

Gravar no DataStore

O 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 atualiza os dados de maneira transacional em uma operação atômica de leitura-gravação-modificação. Todo o código no bloco updateData é tratado como uma única transação.

Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

JSON DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy { exampleCounter = exampleCounter + 1 }
    }
}

Usar o DataStore em um app do Compose

Para usar o DataStore em um app do Compose, siga as diretrizes de arquitetura de apps Android, mantendo as operações do DataStore na camada de dados (como um repositório) e expondo os dados à interface usando um ViewModel.

Evite ler ou gravar no DataStore diretamente nas funções combináveis.

  1. Exponha o DataStore usando um ViewModel. Transmita seu repositório (que envolve o DataStore) para o ViewModel e converta o Flow em um StateFlow para que a interface possa observá-lo facilmente, conforme mostrado no snippet a seguir:

    class SettingsViewModel(
        private val userPreferencesRepository: UserPreferencesRepository
    ) : ViewModel() {
    
        // Expose the DataStore flow as a StateFlow for Compose
        val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = UserSettings.getDefaultInstance()
            )
    
        fun updateCounter(newValue: Int) {
            viewModelScope.launch {
                userPreferencesRepository.updateCounter(newValue)
            }
        }
    }
    
  2. Observe e grave no elemento combinável. Use collectAsStateWithLifecycle para observar com segurança o StateFlow na interface e chame as funções ViewModel para processar gravações, conforme mostrado no snippet a seguir:

    @Composable
    fun SettingsScreen(
        viewModel: SettingsViewModel = viewModel()
    ) {
        // Safely collect the state
        val settings by viewModel.userSettings.collectAsStateWithLifecycle()
    
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Current counter: ${settings.counter}")
    
            Spacer(modifier = Modifier.height(8.dp))
    
            Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) {
                Text("Increment Counter")
            }
        }
    }
    

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 propriedades de consistência presentes em um único processo. Especificamente, o DataStore fornece as seguintes propriedades:

  • As leituras retornam apenas os dados mantidos no disco.
  • Consistência de leitura após gravação.
  • 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 em que o serviço está sendo executado em um processo separado e atualiza periodicamente o DataStore.

Este exemplo usa um repositório de dados JSON, mas também é possível usar um Preferences ou Proto DataStore.

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

Um 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. Consulte abaixo um exemplo de implementação usando a kotlinx.serialization:

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

    override suspend fun readFrom(input: InputStream): Time =
        try {
            Json.decodeFromString<Time>(
                input.readBytes().decodeToString()
            )
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read Time", serialization)
        }

    override suspend fun writeTo(t: Time, output: OutputStream) {
        output.write(
            Json.encodeToString(t)
                .encodeToByteArray()
        )
    }
}

Para usar DataStore em diferentes processos, você precisa criar o objeto DataStore usando a MultiProcessDataStoreFactory para o app e o código de serviço:

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.filesDir.path}/time.pb")
    },
    corruptionHandler = null
)

Adicione o seguinte ao seu AndroidManifiest.xml:

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

O serviço chama updateLastUpdateTime periodicamente, que grava no repositório de dados usando updateData.

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time ->
        time.copy(lastUpdateMillis = System.currentTimeMillis())
    }
}

O app lê o valor gravado pelo serviço usando o fluxo de dados:

fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
    time.lastUpdateMillis
}

Agora, podemos reunir todas essas funções em uma classe chamada MultiProcessDataStore e usá-la em um app.

Confira o código de serviço:

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

E o código do app:

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Last updated: $lastUpdateTime",
    fontSize = 25.sp
)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose {
        context.stopService(serviceIntent)
    }
}

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

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

Processar corrupção de arquivos

Em raras ocasiões, o arquivo persistente do DataStore no disco pode ser corrompido. Por padrão, o DataStore não se recupera automaticamente da corrupção, e as tentativas de leitura causam a geração de uma CorruptionException pelo sistema.

O DataStore oferece uma API de gerenciador de corrupção que pode ajudar você a se recuperar normalmente em um cenário como esse e evitar a geração da 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 gerenciador, 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.filesDir.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

Blogs

Codelabs