Baguingi
Baguingi

Reputation: 61

Jetpack Compose: mutableStateOf doesn't update with flow

I have a ViewModel which uses Flow to get a Note object from my Room database:

var uiState by mutableStateOf(NoteUiState())
        private set


    private fun getNoteById(argument: Int) {
        viewModelScope.launch {
            try {
                repository.getNoteById(argument).collect { note ->
                    uiState = NoteUiState(note = note)
                }
            } catch (e: Exception) {
                uiState = NoteUiState(error = true)
            }
        }
    }

Note class:

@Entity(tableName = "notes")
data class Note(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "title") val title: String = "",
    @ColumnInfo(name = "text") val text: String = "",
) {
    override fun toString() = title
}

This approach works fine, until I try to make a mutable strings with the values of the Note object as their default so I can update 2 TextField composables:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Note(
    note: DataNote,
    isNewNote: Boolean,
    createNote: (DataNote) -> Unit,
    updateNote: (DataNote) -> Unit,
    back: () -> Unit,
    trued: String,
) {

    var title by remember { mutableStateOf(note.title) }
    var content by remember { mutableStateOf(note.text) }
    TextField(
        value = title,
        onValueChange = { title = it },
        modifier = Modifier
            .fillMaxWidth(),
        placeholder = { Text(text = "Title") },
        colors = TextFieldDefaults.textFieldColors(
            containerColor = Color.Transparent
        )
    )
    TextField(
        value = content,
        onValueChange = { content = it },
        modifier = Modifier
            .fillMaxWidth(),
        placeholder = { Text(text = "Content") },
        colors = TextFieldDefaults.textFieldColors(
            containerColor = Color.Transparent
        )
    )
}

For some reason the first time the Note object is called it's null, so I want a way to update the title and content variables.

The Note object itself updates without issue, however the title and content variables never change from the initial value. How can I update the title and content variables while also making them work for the textfield?

Upvotes: 4

Views: 4324

Answers (3)

Pedro Antonio
Pedro Antonio

Reputation: 290

You should think about state hoisting and about having a single source of truth.

Really you need to define where your state will live, if on the viewmodel or on the composable functions.If you are only going to use the state (your note) on the ui then it's ok to hoist your state up to the Note composable function.

But if you need that state in something like another repo, insert it somewhere else or in general to do operations with it, the probably you should hoist it up to the viewmodel (you already have it there).

So use the property of your viewmodel directly in your composable and add a function in your viewmodel to mutate the state and pass this function to the onValueChanged lambda.

Upvotes: 1

Baguingi
Baguingi

Reputation: 61

I found out how to make the Textfield work while also getting the inital value from the object. The issue was that the Note object was called as null on the first call, so the mutableStateFlow didnt get the initial values.

First, I had to pass the actual state as a MutableStateFlow to my composable:

@Composable
private fun Note(
    state: MutableStateFlow<NoteUiState>,
    createNote: (DataNote) -> Unit,
    updateNote: (DataNote) -> Unit,
    back: () -> Unit
) {
...

Next, I just had to get the Note object by calling collectAsState():

val currentNote = state.collectAsState().value.note

Finally, all that was needed was to pass the currentNote object text and title in the value of the Textfield, and on onValueChange to update the state object itself via a copy:

This is the complete solution:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Note(
    state: MutableStateFlow<NoteUiState>,
    createNote: (DataNote) -> Unit,
    updateNote: (DataNote) -> Unit,
    back: () -> Unit
) {
    val currentNote = state.collectAsState().value.note
    Column(Modifier.fillMaxSize()) {
        TextField(
            value = currentNote.title,
            onValueChange = {
                state.value = state.value.copy(note = currentNote.copy(title = it))
                            },
            modifier = Modifier
                .fillMaxWidth(),
            placeholder = { Text(text = "Title") },
            colors = TextFieldDefaults.textFieldColors(
                containerColor = Color.Transparent
            )
        )
    }
}

I'm not sure is this is a clean solution, but is the only way it worked for me, thanks for the feedback, opinions on this approach are always welcomed.

Upvotes: 2

Abhijith mogaveera
Abhijith mogaveera

Reputation: 1304

 var title by remember { mutableStateOf(note.title) }
    var content by remember { mutableStateOf(note.text) }

remember block executes only on 1st composition and then value will remembered until decomposition or u need to change it externally through '=' assignment operator,

.

instated of this

TextField(
        value = title)

write this way

 TextField(
            value = note.title)

enter image description here

Upvotes: 0

Related Questions