Please can someone throw any light on the problem that I have with Jetpack Compose and a ViewModel.
I have a data class called ProductEntity.
data class ProductEntity(
var id: Int = 0,
var name: String = "",
var quant: Int = 1,
)
In a ViewModel I have:
- private local list of products,
- Boolean button state and
- a Products list which is a MutableState<List<ProductEntity>>. This is set to start with the values from the local list of products
- a function to update the product list based on a counter number passed
class TestGlobalViewModel : ViewModel() {
private val localList = ArrayList<ProductEntity>().apply {
add(ProductEntity(name = "a"))
add(ProductEntity(name = "b"))
add(ProductEntity(name = "c"))
}
var buttonState by mutableStateOf(false)
var products = mutableStateOf<List<ProductEntity>>(
localList
)
fun updateProducts(counter:Int){
val localList2 = ArrayList<ProductEntity>()
localList2.add(ProductEntity(name = "a$counter"))
localList2.add(ProductEntity(name = "b$counter"))
localList2.add(ProductEntity(name = "c$counter"))
localList2.add(ProductEntity(name = "a${counter + 1}"))
localList2.add(ProductEntity(name = "b${counter + 1}"))
localList2.add(ProductEntity(name = "c${counter + 1}"))
localList2.add(ProductEntity(name = "a${counter + 3}"))
localList2.add(ProductEntity(name = "b${counter + 3}"))
localList2.add(ProductEntity(name = "c${counter + 3}"))
products = mutableStateOf(localList2)
}
}
I have the HomeScreen Composable
In there I have:
- a reference to the ViewModel
- mutable state of a counter
- mutable state of a filtered list. This latter list is updated whenever the Products list is replaced.
I have a Button that displays the first item in the filtered list and does three things when pressed
- Increments the counter
- updates the product list
- toggles the buttonState variable in the view model
Finally I have a LaunchedEffect triggered by the button state that does nothing.
@Composable
fun HomeScreen() {
val vm = viewModel<TestGlobalViewModel>()
var counter by remember {
mutableStateOf(1)
}
var filteredList by remember {
mutableStateOf<List<ProductEntity>>(emptyList())
}
filteredList = run {
val filtered = ArrayList<ProductEntity>()
vm.products.value.forEach {
if (it.name.contains("a")) filtered.add(it)
}
filtered
}
LaunchedEffect(vm.buttonState) {}
TestTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Button(onClick = { // temporary print
counter += 1
vm.updateProducts(counter)
Log.i("CEtest", // print out the filtered list
buildString {
filteredList.forEach {
append("filteredList = $it \n")
}
})
vm.buttonState = vm.buttonState == false
}) {
Text(text = filteredList[0].name)
}
}
}
}
This works perfectly until either the LaunchedEffect is removed or the button state is not changed. At that point nothing works – the button text is not updated and the filtered list is not changed.
I cannot see how this is occurring and why I need the button state LaunchedEffect to trigger the rest of the process.
Any help you can give would be gratefully appreciated.
Thanks
>Solution :
Consider making products a MutableState variable in the ViewModel itself. This way, Compose can directly observe changes to this state and trigger recomposition accordingly.
class TestGlobalViewModel : ViewModel() {
// ...
var products by mutableStateOf(localList)
private set
fun updateProducts(counter: Int) {
// ... (no need to create a new mutableStateOf)
products = localList2
}
}
If you’re using Jetpack Compose with ViewModels, it’s recommended to use LiveData in your ViewModel and observe it in Compose. This way, Compose can automatically handle the observation and recomposition.
class TestGlobalViewModel : ViewModel() {
private val _products = MutableLiveData<List<ProductEntity>>(localList)
val products: LiveData<List<ProductEntity>> get() = _products
fun updateProducts(counter: Int) {
// ...
_products.value = localList2
}
}
In your Composable:
val products by vm.products.observeAsState(emptyList())