How to update a `mutablestate` uistate to set curr...
# compose
p
How to update a
mutablestate
uistate to set current selected item in a viewmodel? I have this uistate class, and need to change
selectedBookId
value.
Copy code
sealed interface BooksGridUiState {
    data class Success(
        val booksList: List<BookData>,
        val selectedBookId: String?
    ) : BooksGridUiState
    object Error : BooksGridUiState
    object Loading : BooksGridUiState
}

var uiState: BooksGridUiState by mutableStateOf(BooksGridUiState.Loading)
        private set
Can't do the trick of calling
Copy code
.update {
    it.copy(
Because this is not a
mutablestateflow
, and I'm following a codelab and must use
mutablestate
is it correct to do it this way?
Copy code
fun selectBook(id: String) {
    (uiState as BooksGridUiState.Success).let {
        uiState = BooksGridUiState.Success(it.booksList, id)
    }
}
maybe is better to do it this way?
Copy code
fun selectBook(id: String) {
    (uiState as BooksGridUiState.Success).let {
        uiState = it.copy(selectedBookId = id)
    }
}
v
I think people will need a bit more context to be able to help. What does your viewmodel look like?
p
Copy code
sealed interface BooksGridUiState {
    data class Success(
        val booksList: List<BookData>,
        val selectedBookId: String? = null
    ) : BooksGridUiState
    object Error : BooksGridUiState
    object Loading : BooksGridUiState
}

class BooksGridViewModel(
    val bookshelfRepository: BookshelfRepository
): ViewModel() {
    var uiState: BooksGridUiState by mutableStateOf(BooksGridUiState.Loading)
        private set

    init {
        getBooks()
    }

    private fun getBooks() {
        viewModelScope.launch {
            try {
                uiState = BooksGridUiState.Success(bookshelfRepository.getBooks())
            } catch (e: Exception) {
                e.printStackTrace()
                uiState = BooksGridUiState.Error
            }
        }
    }

    fun selectBook(id: String?) {
        (uiState as? BooksGridUiState.Success)?.let {
            uiState = it.copy(selectedBookId = id)
        }
    }

    /**
     * Factory for this viewmodel that takes repository as a dependency
     */
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as BookshelfApplication)
                val repository = application.container.booksRepository
                BooksGridViewModel(repository)
            }
        }
    }
}
v
I don't think there's a right or wrong answer here. That is one variation that does work blob smile
One thing to note in the VM otherwise is that
e.printStackTrace()
prints to stderr instead of logcat.
Also not sure why you need a separate viewmodel factory, but I'm not very deep into hilt
p
thank you, maybe can you give me your opinion here too? https://kotlinlang.slack.com/archives/CJLTWPH7S/p1716983427553509
z
You could also make the selectedId property inside Success a var backed by a mutable state as well
p
i thought that all the values inside the uistate should be val, not var
a
You can’t use > .update { > it.copy( because your UiState is declared as
sealed interface
, not data class. So for you to be able to update the
selectedBookId
, you need to create a
new Success UiState
with the selectedBookId you want to update.
v
i thought that all the values inside the uistate should be val, not var
It's sometimes convenient to have a single UI state object (with no sub-state) but it doesn't have to be so in every case. It's difficult to give definite guidelines because there are so many different possible scenarios. For example if your data has several subsections that usually change as a group or very frequently, it might make sense to extract those to a separate state.
@Anselmo Alexandre not sure what you are trying to say,
.update()
cannot be used because the object is not a
MutableStateFlow
, which has the extension method:
Copy code
public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T)
a
@vide as I understood his question, he wants to
copy()
selectedBookId
to the already existing uiState. Right?
v
Yes, but I didn't understand how this was related to the sealed interface 😅
a
Got the question wrong then 😅
v
No worries 😄 I think the question was about the style of updating based on the examples
Copy code
uiState = BooksGridUiState.Success(it.booksList, id)
uiState = it.copy(selectedBookId = id)
a
uiState = BooksGridUiState.Success(it.booksList, id)
So this was what I was referring to, create a hole new instance of BooksGridUiState.Success and then update it, because he wouldn’t be able to use copy() on sealed class/interface.
uiState = it.copy(selectedBookId = id)
For this style to be achieved, he would have to declare the BooksGridUiState has data class, like
data class BooksGridUiState(…)
, so afterwards on new uiState, inside the
.update{ it.copy(selectedBookId = id) }
v
Ah I think there might be a confusion on how sealed interfaces work. You can't create other direct subclasses of
BooksGridUiState
but you can create new instances of
Success
.
it.copy(selectedBookId = id)
works just fine!
Because
Success
is not sealed, right?
What the sealed interface prevents is this:
val a = object : BooksGridUiState { }
a
Yep,
z
i thought that all the values inside the uistate should be val, not var
This may depend on your architecture, but all that compose requires is that if they’re
var
, that they’re backed by snapshot state