Como Implementar o DataStore de Preferências no Compose Multiplataforma

O Preferences DataStore é uma solução moderna para armazenar pequenas quantidades de dados chave-valor no Android, substituindo o SharedPreferences. Ele se destaca pela eficiência, utilizando o Kotlin Flow para o tratamento assíncrono de dados e garantindo a consistência das informações. Este guia apresenta a configuração do DataStore em Compose Multiplatform, permitindo armazenar e recuperar preferências chave-valor em ambas as plataformas.

Para facilitar a organização e injeção de dependências, será utilizada a biblioteca Koin, um framework DI leve e fácil de usar. O Preferences DataStore será configurado separadamente para Android e iOS, com o Koin fornecendo instâncias dessas soluções de armazenamento.

Configurando o DataStore em Compose Multiplatform

Para começar, adicione as dependências necessárias no seu arquivo *libs.versions.toml*:

Versões
datastore 1.1.3
koin 3.5.6
koinCompose 1.1.5
koinComposeViewModel 1.2.0-Beta4

No seu módulo compartilhado/composeApp (build.gradle.kts), adicione as seguintes implementações:

sourceSets {
        androidMain.dependencies {
            implementation(libs.koin.android)
        }
        commonMain.dependencies {
            implementation(libs.koin.core)
            implementation(libs.koin.compose)
            implementation(libs.koin.compose.viewmodel)

            implementation(libs.datastore)
            implementation(libs.datastore.preferences)
        }
}

Criando uma instância do DataStore

Crie uma instância do DataStore em Compose Multiplatform no pacote commonMain. Salve o seguinte código como DataStoreInstance.kt em commonMain:

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized
import okio.Path.Companion.toPath

@OptIn(InternalCoroutinesApi::class)
private val lock = SynchronizedObject() // Usado para segurança de threads
private lateinit var dataStore: DataStore

@OptIn(InternalCoroutinesApi::class)
fun createDataStore(producePath: () -> String): DataStore {
    return synchronized(lock) {
        if (::dataStore.isInitialized) {
            dataStore
        } else {
            PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() })
                .also { dataStore = it }
        }
    }
}

internal const val DATA_STORE_FILE_NAME = "storage.preferences_pb"

No Android, chame essa função com o argumento de caminho (DataStoreInstance.android.kt):

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences

fun createDataStore(context: Context): DataStore {
    return createDataStore {
        context.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath
    }
}

E, no iOS (DataStoreInstance.ios.kt):

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask

@OptIn(ExperimentalForeignApi::class)
fun createDataStore(): DataStore {
    return createDataStore {
        val directory = NSFileManager.defaultManager.URLForDirectory(
            directory = NSDocumentDirectory,
            inDomain = NSUserDomainMask,
            appropriateForURL = null,
            create = false,
            error = null,
        )

        requireNotNull(directory).path + "/$DATA_STORE_FILE_NAME"
    }
}

Configurando o Koin para injeção de dependência

Com a configuração do DataStore completa, configure o Koin para injeção de dependência, injetando ou fornecendo o DataStore no seu repositório ou ViewModel.

No commonMain, crie um DataStoreModule com a variável esperada:

commonMain (DataStoreModule.kt)

import org.koin.core.module.Module

expect val dataStoreModule: Module

Adicione a declaração real em androidMain e iosMain:

androidMain (DataStoreModule.android.kt)

import com.rainday.datastorecmp.createDataStore
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.Module
import org.koin.dsl.module

actual val dataStoreModule: Module
 get() = module { single { createDataStore(androidContext()) } }

iosMain (DataStoreModule.ios.kt)

actual val dataStoreModule: Module
    get() = module { single { createDataStore() } }

Na implementação do Android, é necessário um Context para criar o Preferences DataStore, enquanto no iOS não há dependência de contexto. Para lidar com essa diferença, você pode modificar a função de inicialização do Koin (initKoin) para aceitar uma função de configuração (config). Isso permite configurar o contexto específico do Android sem afetar a plataforma iOS.

// commonMain
fun initKoin(
    config: (KoinApplication.() -> Unit)? = null
) {
    startKoin {
        config?.invoke(this)

        modules(dataStoreModule)
    }
}

Inicialize o Koin no Android e iOS:

androidMain (Application Class)

class YourApplicationClass: Application() {
    override fun onCreate() {
        super.onCreate()
        initKoin(
            config = {
                androidContext(this@BaseApplication)
            }
        )
    }
}

iosMain (MainViewController.kt)

fun MainViewController() = ComposeUIViewController(
    configure = {
        initKoin()
    }
) { App() }

Implementando o DataStore em um ViewModel

O Preferences DataStore está pronto para ser usado em um repositório ou ViewModel. Veja um exemplo de como usá-lo:

ViewModel em pacote commonMain:

class AppViewModel(
    private val dataStore: DataStore
): ViewModel() {

    private val key = stringPreferencesKey("name")

    private var _name = MutableStateFlow("")
    val name = _name.asStateFlow()

    init {
        viewModelScope.launch {
            dataStore.data.collect { storedData ->
                _name.update {
                    storedData.get(key).orEmpty()
                }
            }
        }
    }

    fun updateName(name: String) = _name.update { name }

    fun storeToDataStore() {
        viewModelScope.launch {
            dataStore.updateData {
                it.toMutablePreferences().apply {
                    set(key, name.value)
                }
            }
        }
    }
}

Defina um módulo Koin em commonMain que fornece uma instância AppViewModel para injeção de dependência.

val viewModelModule = module {
    viewModel { AppViewModel(get()) } 
// automaticamente injetando os parâmetros necessários usando get()
}

Atualize a função initKoin:

fun initKoin(
    config: (KoinApplication.() -> Unit)? = null
) {
    startKoin {
        config?.invoke(this)

        modules(viewModelModule, dataStoreModule) // add viewModelModule
    }
}

Camada de UI (Interface de Usuário Compartilhada com Jetpack Compose)

Injete AppViewModel em Composable App():

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.KoinContext
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI

@OptIn(KoinExperimentalAPI::class)
@Composable
@Preview
fun App() {
    MaterialTheme {
        KoinContext {
            val viewModel = koinViewModel()

            val name by viewModel.name.collectAsStateWithLifecycle()

            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Column(
                    modifier = Modifier.padding(16.dp)
                ) {
                    TextField(
                        value = name,
                        onValueChange = viewModel::updateName,
                        label = { Text("Name") }
                    )

                    Button(onClick = viewModel::storeToDataStore, modifier = Modifier.padding(top = 8.dp)) {
                        Text("Store")
                    }
                }
            }
        }
    }
}

Agora o Preferences DataStore está totalmente configurado e pronto para uso em Android e iOS.

Este conteúdo foi auxiliado por Inteligência Artificiado, mas escrito e revisado por um humano.

Via dev.to

Leave a Comment

Bibliotecas
datastore { module = “androidx.datastore:datastore”, version.ref = “datastore” }
datastore-preferences { module = “androidx.datastore:datastore-preferences”, version.ref = “datastore” }
koin-core { module = “io.insert-koin:koin-core”, version.ref = “koin” }
koin-compose { module = “io.insert-koin:koin-compose”, version.ref = “koinCompose” }
koin-compose-viewmodel { module = “io.insert-koin:koin-compose-viewmodel”, version.ref = “koinComposeViewModel” }
koin-android { module = “io.insert-koin:koin-android”, version.ref = “koin” }