NullPointerException
NullPointerException

Reputation: 37721

How to init TextField with datastore variable

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") }
    )

Upvotes: 0

Views: 165

Answers (2)

Alba
Alba

Reputation: 1

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

Upvotes: 0

NullPointerException
NullPointerException

Reputation: 37721

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.

Upvotes: 0

Related Questions