Konstantin Schubert
Konstantin Schubert

Reputation: 3356

Jetpack Compose: Correct way to let a usr edit a text value contained in a StateFlow object

I have a ViewModel containing a MutableStateFlow called uiState.

The uiState contains a string value called myString which is originally initialised from a repository ( remote API), but then should be editable by the user. Whenever the user edits it, it should be saved back to the repository.

class MyViewModel {
    private val _uiState = MutableStateFlow(MyUiState())
    val uiState: StateFlow<MyUiState> = _uiState.asStateFlow()
}

My understanding is that you should not directly connect a StateFlow value uiState.myString to a OutlinedTextField. For example, this is clearly stated in this blog post here: https://medium.com/androiddevelopers/effective-state-management-for-textfield-in-compose-d6e5b070fbe5

So what is the correct approach to achieve the desired behaviour?

Here is what I am currently thinking of, but I have no idea if that's a good idea:

Inside the Compose function, define a local string variable that is originally initialised by the value of uiState.myString, and then, when the input is complete, update first the repository and then uiState.myString.


@Composable
fun MyTextEdit(
    initialValue: String // this arguments gets passed `uiState.myString` when
    onKeyboardDone: () -> Unit,
) {
    val editString: String = remember { mutableStateOf(default= initialValue) }
    OutlinedTextField(
    value = editString,
    onValueChange = { newValue => editString = newValue} 
    keyboardActions = KeyboardActions(
                    onDone = { saveToRegistryAndThenUpdateMyStringInUiState() }
    )

[...]
// And then the composable is called like this:

@Composable
fun MyUi(
    val uiState by MyViewModel.uiState.collectAsState()
    MyTextEdit(
        initialValue=uiState.myString,
        onKeyboardDone = { newValue ->
              saveToRepository(newValue){ 
                    savedValue -> uiState.update {
                        currentState ->
                        currentState.copy(myString = newValue)
                    }
              }
         }
)

Will this work? Is this the best solution?

Upvotes: 1

Views: 704

Answers (2)

Megh Lath
Megh Lath

Reputation: 2214

Its better to save newValue after some delay in repository whenever user inputs value instead of depending on keyboard's onDone action.

For delay - you can use debounce

Here is basic example:

ViewModel
const val DEBOUNCE_DELAY = 600L

val newValue = MutableStateFlow("")

init {
    // This will be called at delay of 600MS whenever `newValue` is updated
    viewModelScope.launch {
        newValue.debounce {
            if (it.isEmpty()) 0 else DEBOUNCE_DELAY
        }.flowOn(appDispatcher.IO)
            .collectLatest { newStr ->
                if (newStr.isNotEmpty()) {
                    withContext(Dispatcher.IO) {
                            // Save newValue to repository & state
                         }
                    }
                }
            }
    }
}


fun onValueChange(newStr: String) {
    newValue.value = newStr
}

In View
@Composable
fun MyTextEdit(
    initialValue: String // this arguments gets passed `uiState.myString` when
    onKeyboardDone: () -> Unit,
) {
    val editString: String = remember { mutableStateOf(default= initialValue) }
    OutlinedTextField(
        value = editString,
        onValueChange = { newValue => MyViewModel.onValueChanged(newValue) } 
    )

[...]

Upvotes: 0

Chirag Thummar
Chirag Thummar

Reputation: 3242

For the best TextField Best practices you can directly use mutableStateOf inside the ViewModel and ViewModel will keep track of it when configuration changes.

  1. You can define a Text variable inside the ViewModel and get it Inside the screen.

  2. Make a function which will update the String value in ViewModel.

Stack07ViewModel.kt

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel

class Stack07ViewModel : ViewModel() {

    var textFieldValue by mutableStateOf("")
        private set

    fun updateTextField(str: String) {
        textFieldValue = str
    }
}

You can use in your screen as below

We are passing the initial value from the ViewModel and whenever TextField value changes we call updateTextField function in ViewModel.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.md.newcomposeplayground.stackoverflowq.viewmodels.Stack07ViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Stack07(stack07ViewModel: Stack07ViewModel) {

    Scaffold (
        topBar = {
            TopAppBar(title = {
                Text(text = "Text Field Demo")
            },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.primary
                ))
        }
    ){
        Column (modifier = Modifier
            .padding(it)
            .fillMaxSize()
            .padding(10.dp)){
            OutlinedTextField(
                modifier = Modifier.fillMaxWidth(),
                value = stack07ViewModel.textFieldValue,
                onValueChange = {
                    stack07ViewModel.updateTextField(it)
                },
                placeholder = {
                    Text(text = "Enter your text")
                }
            )
        }
    }
}

Upvotes: 0

Related Questions