'Composable not recomposing when ViewModel updated from a different Composable

I am developing an Android app using Jetpack Compose. The app shows a list of items. It also has a top bar containing a search bar.

We have 3 composable: the list of items, the search bar component, and the app bar (the app bar contains the search bar).

The same ViewModel class is injected in the list composable and the search bar composable (using Hilt). The ViewModel has the list of items:

var items by mutableStateOf<List<Item>?>(null)

as well as a search function, called by the search bar, to filter these items.

The problem is the following: the list composable is not recomposing when searching items.

NB:

  • I log the output of the search function so I am sure that the function is working fine.
  • When placing the searchbar composable in the same composable containing the list of items, the search works as expected.

So it seems that the list is recomposing only when the searchbar is in it.

How to make it working when the searchbar is in the app bar?

Thank you,

Edit: By generating a random value when a ViewModel is initialized, it seems that Hilt is injecting two different instances in Searchbar composable and items composable.

Is it possible to inject ViewModel as a singleton? or should I save my data in the repository and then serve these data to the ViewModel?

Here's the code of the different classes:

Searchbar.kt

@Composable
fun SearchBar(itemViewModel: ItemViewModel = hiltViewModel()){
var searchText by remember { mutableStateOf("") }

    BasicTextField(
        singleLine = true,
        cursorBrush = SolidColor(Color.White),
        value = searchText,
        onValueChange = {
            searchText = it
            itemViewModel.searchByLabel(it)
        },
        modifier = Modifier
            .fillMaxWidth()
            .height(28.dp),
        textStyle = LocalTextStyle.current.copy(
            color = Color.White,
            fontSize = MaterialTheme.typography.body2.fontSize
        ),
        decorationBox = { innerTextField ->
            Row(
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.spacedBy(4.dp)
            ) {
                Icon(
                    Icons.Filled.Search,
                    contentDescription = "Search"
                )
                Column(Modifier.weight(1f)) {
                    Box(
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        if (searchText.isEmpty()) {
                            Text(
                                "Search",
                                style = LocalTextStyle.current.copy(
                                    color = Color.White.copy(alpha = 0.7f),
                                    fontSize = MaterialTheme.typography.body2.fontSize
                                )
                            )
                        }
                        innerTextField()
                    }
                }
                if (searchText.isNotEmpty()) {
                    IconButton(onClick = { searchText = "" }) {
                        Icon(Icons.Filled.Cancel, contentDescription = "Delete")
                    }
                }
            }
        }
    )
}

Items.kt

@Composable
fun Items(
    navController: NavController,
    itemViewModel: ItemViewModel = hiltViewModel(),
){
    val items = itemViewModel.items
    items?.let { items ->
        LazyColumn(
            modifier = Modifier.fillMaxWidth(),
            verticalArrangement = Arrangement.spacedBy(24.dp)
        ) {
    
            items(items) { item ->
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable {
                            navController.navigate("items/${item.id}")
                        },
                    Arrangement.spacedBy(8.dp)
                ) {
                    Text(item.label)
                }
            }
        }
    } ?: run {
        Text(
            text = "No protocols",
            modifier = Modifier.fillMaxWidth()
        )
    }
}

ItemViewModel.kt

@HiltViewModel
class ItemViewModel @Inject constructor(private val repository: ItemRepository) : ViewModel() {
     var items by mutableStateOf<List<Item>?>(null)

     init{ loadItems() }

     private fun loadItems(){
         viewModelScope.launch {
             items = repository.getItems()
         }
     }

     fun searchByLabel(value: String){
         viewModelScope.launch {
             items = repository.search(value)
         }
     }
}

ItemRepository.kt

class ItemRepository(){
    suspend fun getItems(): List<Item>? {
        return listOf(Item(1, "Label 1"), Item(2, "Label 2"), Item(3, "Label 3"))
    }

    suspend fun search(value: String): List<Item>? {
        return getItems()?.filter { it.label.lowercase().contains(value.lowercase())}
    }
}

Item.kt

data class Item(var id: Int, var label: String){}


Solution 1:[1]

I ended up storing data in the repository, which is injected as singleton. This way, even if multiple instances of a ViewModel are created, they still access the same data.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 nawrasg