1

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

1 Answers1

1

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.
    }
}
Johann
  • 27,536
  • 39
  • 165
  • 279
  • I encountered two problems using your solution. First, saving TextField value on focus changes may save an empty string depending on when the TextField changes focus(like gaining focus before the initial text value has been set). So using a save button is probably the better way. – Shahryar Khosravi Dec 03 '21 at 13:14
  • Well you can check if the string is empty before saving and not bother to save it. What's the second problem? – Johann Dec 03 '21 at 13:16
  • Second problem is that, when initializing text value inside mutableStateOf, apiKey has not yet received any value from dataStore so it is null at the point of first composition. Since we are not observing the apiKey as a State, our composable won't get recomposed on any changes that are received by the liveData. – Shahryar Khosravi Dec 03 '21 at 13:21
  • If your text is stored locally in a data store, you can use state hoisting and retrieve the text and pass that into your composable for the intial value. viewmodel.getApiKey() should just return plain text without any state management. I updated the code to demonstrate this. – Johann Dec 03 '21 at 13:41
  • BTW, you don't need LiveData for your text. It doesn't make sense to monitor data changes for a TextField. You simply provide the initial data and then update it when the focus changes or when a button is pressed. – Johann Dec 03 '21 at 13:51
  • Thank you for your help, I also found a youtube video by Android Developers explaining exactly the same problem: [https://youtu.be/mymWGMy9pYI](https://youtu.be/mymWGMy9pYI) – Shahryar Khosravi Dec 03 '21 at 14:19