I'm trying to implement a "settings" screen for my app using DataStore pereferences. Basically, an url is created, according to the preferences, so a WebView can load it later. The data is saved correctly, and the UI from the settings screen is updating the values when a preference is changed.
The problem is that I found out, when trying to load an updated url, that the preferences are not read instantly (the WebView was loading a previous url value after being updated). So my question is how to convert it to that type so the preferences are read in real time?
Here are some code snippets:
UserPreferences Data class
data class UserPreferences(
val conexionSegura: Boolean,
val servidor: String,
val puerto: String,
val pagina: String,
val parametros: String,
val empresa: String,
val inicioDirecto: Boolean,
val usuario: String,
val password: String,
val url: String
)
DataStore repository impl getPreferences()
class DataStoreRepositoryImpl @Inject constructor(
private val dataStore: DataStore<Preferences>
) : DataStoreRepository {
override suspend fun getPreferences() =
dataStore.data
.map { preferences ->
UserPreferences(
conexionSegura = preferences[PreferencesKeys.CONEXION_SEGURA] ?: false,
servidor = preferences[PreferencesKeys.SERVIDOR] ?: "",
puerto = preferences[PreferencesKeys.PUERTO] ?: "",
pagina = preferences[PreferencesKeys.PAGINA] ?: "",
parametros = preferences[PreferencesKeys.PARAMETROS] ?: "",
empresa = preferences[PreferencesKeys.EMPRESA] ?: "",
inicioDirecto = preferences[PreferencesKeys.INICIO_DIRECTO] ?: false,
usuario = preferences[PreferencesKeys.USUARIO] ?: "",
password = preferences[PreferencesKeys.PASSWORD] ?: "",
url = preferences[PreferencesKeys.URL] ?: CONTENT_URL
)
}
...
...
ViewModel readPreferences()
private val _state = mutableStateOf(SettingsState())
val state: State<SettingsState> = _state
init {
readPreferences()
updateUrl()
}
private fun readPreferences() {
viewModelScope.launch {
dataStoreRepositoryImpl.getPreferences().collect {
_state.value = state.value.copy(
conexionSegura = it.conexionSegura,
servidor = it.servidor,
puerto = it.puerto,
pagina = it.pagina,
parametros = it.parametros,
empresa = it.empresa,
inicioDirecto = it.inicioDirecto,
usuario = it.usuario,
password = it.password
)
}
}
}
...
...
After investigating and reading a little bit, I realised that DataStore emits flows, not stateFlows, which would be needed in compose. I thought about deleting the UserPreferences class, and simply read one preference at a time, collecting it asState inside the screen composable. But, I think the code would be cleaner of I don't do that :) I really like the idea of using a separate class to hold the preferences' values
CodePudding user response:
When you want to transform a Flow
into a StateFlow
in a ViewModel to be consumed in the view, the correct way is using the stateIn() method.
Use of stateIn
Assuming you have the following class and interface :
class SettingsState()
sealed interface MyDatastore {
fun getPreferences(): Flow<SettingsState>
}
In your viewModel, create a val that will use the datatStore.getPreferences method and transform the flow into a stateFlow using stateIn
class MyViewModel(
private val dataStore: MyDatastore
) : ViewModel() {
val state: StateFlow<SettingsState> = dataStore
.getPreferences()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SettingsState()
)
}
Collect in the composable
To start getting preferences, you have just to collect the stateFlow as state :
@Composable
fun MyComposable(
myViewModel: MyViewModel = hiltViewModel()
) {
val state = myViewModel.state.collectAsState()
//...
}
Pros
As you can see, you don't need to use init
in the ViewModel
. Like 90% of time, use of init
is not required. The ViewModel became more testable because you don't need to mock everything that is in the init
block.