Search code examples
androidviewviewmodelandroid-jetpack-composeandroid-jetpack-datastore

Using viewModel as the single source of truth in jetpack compose


let's say we have a viewModel that has a value called apiKey inside. Contents of this value is received from DataStore in form of a Flow and then, it is exposed as LiveData. On the other hand we have a Fragment called SettingsFragment, and we are trying to display that apiKey inside a TextField, let the user modify it and save it in DataStore right away. The solution that I'm currently using is down below, but the issue is that the UI gets very laggy and slow when changes are being made to the text. My question is that what is the best way to implement this and still have a single source of truth for our apiKey?

class SettingsViewModel() : ViewModel() {

    val apiKey = readOutFromDataStore.asLiveData()

    fun saveApiKey(apiKey: String) {
        viewModelScope.launch(Dispatchers.IO) {
            saveToDataStore("KEY", apiKey)
        }
    }
}

/** SettingsFragment **/
...

@Composable
fun ContentView() {
    var text = mViewModel.apiKey.observeAsState().value?.apiKey ?: ""

    Column() {
        OutlinedTextField(
            label = { Text(text = "API Key") },
            value = text,
            onValueChange = {
                text = it
                mViewModel.saveApiKey(it)
            })
    }
}

Solution

  • Don't save the TextField's value in the onValueChange event to the data store on every key press - which is almost certainly slowing you down - especially if you are using the same thread. Use a local state variable and only update the data store when the user either moves the focus elsewhere or they save what's on the screen through some button press. You also need to avoid mixing UI threading with data storage threading which should be on the IO thread. Here is one possible solution:

    @Composable
    fun ContentViewHandler() {
        ContentView(
            initialText = viewmodel.getApiKey(),
            onTextChange = { text ->
                viewmodel.updateApiKey(text)
            }
        )
    }
    
    @Composable
    fun ContentView(
        initialText: String,
        onTextChange: (text: String) -> Unit
    ) {
        var text by remember { mutableStateOf(initialText) }
    
        Column() {
            OutlinedTextField(
                label = { Text(text = "API Key") },
                value = text,
                onValueChange = {
                    text = it
                },
                modifier = Modifier.onFocusChanged {
                    onTextChange(text)
                }
            )
    
            // Or add a button and save the text when clicked.
        }
    }