Home > Software engineering >  How can I convert DataStore preferences' Flow to StateFlow inside a viewModel in Jetpack Compos
How can I convert DataStore preferences' Flow to StateFlow inside a viewModel in Jetpack Compos

Time:01-27

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 initis not required. The ViewModel became more testable because you don't need to mock everything that is in the init block.

  • Related