0

Having a composable which has a textfield which needs to work with a by remember mutablestateof String called text, that variable must be initialized with a datastore stored value. I have the datastore repo on the viewmodel, but it returns a Flow, which also needs to be executed in a coroutine. So... I'm stuck.

I can't return directly the Flow and do collectAsStateWithLifecycle because it needs a coroutine, and I can't return the coroutine because it returns a job and I need a string. How to solve this?

This is my datastore variable with the initial value for the field, and this is in my viewmodel:

userPreferencesRepository.searchText

This is the datastore value code, in my datastore repo class:

val searchText: Flow<String> = dataStore.data
    .catch {
        if (it is IOException) {
            Log.e(TAG, "Error reading preferences.", it)
            emit(emptyPreferences())
        } else {
            throw it
        }
    }
    .map { preferences ->
        preferences[SEARCH_TEXT] ?: ""
    }

A sample of a textfield that should receive that text variable from viewmodel, but I don't know how to get it.

viewModel: FlightsScreenViewModel = viewModel(factory = FlightsScreenViewModel.factory)
var text by remember{ mutableStateOf("") } //This should be initialized with the text from the datastore of the viewmodel

TextField(
        value = text,
        onValueChange = { newText ->
            text = newText
        },
        label = { Text("Search") }
    )

2 Answers 2

0

Well, using a flow was not the correct approach, because it initialized the textfield with empty "" string, and 1 or 2 seconds after that, asynchronously, the datastore returned the value, but with the composable already painted with empty string.

I solved it doing this in the viewmodel:

fun getSearchTextFromPreferences() = runBlocking {
        userPreferencesRepository.searchText.first()
    }

and this in the composable:

val initialText = viewModel.getSearchTextFromPreferences()
    var searchText by remember{ mutableStateOf(initialText) }

I had to use runBlocking to force datastore work syncronously with compose, forcing it to wait until get the value before painting the textfield. A better option whould be to paint a loading image until the value is returned, and then paint the textfield with the initial value correctly, I'll try that.

Sign up to request clarification or add additional context in comments.

4 Comments

This is wrong. You are freezing the UI thread, which makes your app seem janky (grinds the whole phone to an halt) and puts you in danger of an ANR. As with anything else that has a load time, you can either initially show blank text, show a placeholder empty screen (which is how most big-name apps behave), or show a load screen/loading indicator, until the value is available.
But the 1-2 second pause seems to point to some other issue. It shouldn’t take that long to load in the first place unless you are storing tens of megabytes of data in your DataStore. If you just had a few key-value pairs, runBlocking would probably be ok because the UI freeze would be so short.
please read the last part of my answer, I put it on bold to clarify
As a rule of thumb: Never use runBlocking (especially in an Android app), and don't collect flows in the view model (that's what first does under the hood). When the need arises then that's an indicator that there is something else wrong with your code. Better focus on fixing that instead.
0

You can try to assign your seachText to text variable when it is initialized with value from database. The idea is to assign value to text variable when the view is recomposed. You will need another variable searchTextLoaded for this.

In your viewModel class remove runBlocking and add function saveSearchTextToPreferences:

fun getSearchTextFromPreferences() = userPreferencesRepository.searchText
fun saveSearchTextToPreferences(text: String) {
    viewModelScope.launch {
        userPreferencesRepository.saveSearchText(text)
    }
}

In your composable:

val searchText = viewModel.getSearchTextFromPreferences().collectAsState("").value
var text by remember { mutableStateOf("") }
var searchTextLoaded by remember { mutableStateOf(false) }

if (searchText.isNotEmpty() && !searchTextLoaded) {
    text = searchText
    searchTextLoaded = true
}

TextField(
    value = text,
    onValueChange = { newText ->
        text = newText
        viewModel.saveSearchTextToPreferences(newText)
    },
    label = { Text("Search") }
)

Here is link to complete app code on github: https://github.com/alba221/flightsearch

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.