Reputation: 478
I have a typical Android MVVM project (Compose -> ViewModel -> Repository -> RoomDB) and have simplified concept in demonstration below.
What am I trying to achieve?: I want to read two flows from a Room Dao (@Query(...) fun getAll(): Flow<List>) from my repository. They should be combined into a single flow and then returned to my viewModel. From the viewModel I want to transform the flow into a StateFlow that can be observing for as long as the viewModel lives. In the example below I propagate this StateFlow value to a mutable state "item" just to be able to display value in UI for demonstration.
The problem: The combine method on the flow doesn't trigger any changes back up in the hierarchy, neither does collectLatest on a single Dao flow when I try debugging. The funny thing is that collect does...
Now onto the demonstration:
class ViewModel(private val coroutineScope: CoroutineScope) {
private val repository = Repository()
val item = mutableStateOf<String?>(null)
init {
coroutineScope.launch {
repository.item.stateIn(coroutineScope).collect {
item.value = it
}
}
}
fun click() {
coroutineScope.launch {
repository.increaseItem()
}
}
}
class Repository {
private val dao = Dao()
val item: Flow<String> = flow {
// This does NOT trigger
dao.dbItem.combine(dao.dbItem2) { value1, value2 ->
emit("$value1|$value2")
}
// This does NOT trigger
// dao.dbItem.collectLatest {
// emit(it)
// }
// This does trigger...
// dao.dbItem.collect {
// emit(it)
// }
}
suspend fun increaseItem() = dao.increaseDbItem()
}
class Dao {
val dbItem = MutableStateFlow("0")
val dbItem2 = MutableStateFlow("A")
suspend fun increaseDbItem() {
dbItem.emit("${dbItem.value} 0")
dbItem2.emit("${dbItem2.value} A")
}
}
@Composable
fun AtteTest() {
val coroutineScope = rememberCoroutineScope()
val viewModel = ViewModel(coroutineScope)
TestAppTheme {
Surface {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Value:")
Text(viewModel.item.value ?: "")
}
Button(onClick = {
viewModel.click()
}) {
Text(text = "click")
}
}
}
}
}
}
@Preview
@Composable
fun PreviewAtteTest() {
AtteTest()
}
Upvotes: 1
Views: 678
Reputation: 16162
There are several issues with your code:
Your view model should extend androidx.lifecycle.ViewModel(). By doing so you can also drop the CoroutineScope parameter and use the inherited viewModelScope
property instead. In your activity you can then retrieve an instance of this view model by val viewModel: ViewModel by viewModels()
and pass it down to your composables. (You should probably rename your view model so it does not share the same name with its parent class.)
As a general rule of thumb, don't collect flows in the view model. What you should do is to directly create a StateFlow
from the repository flow (you can then remove the init
block):
val item: StateFlow<String?> = repository.item.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)
value
property of the flow. What you want instead is to subscribe to the flow by calling any of the collect
methods. If you only access the value
property you retrieve the current value, but if there are no subscribers, the state flow might decide that there is no need to produce new values. What you want is to collect the flow in your composable with val item by viewModel.item.collectAsStateWithLifecycle()
(best location would be where you retrieve the view model and then just pass the item
variable down to your other composables). collectAsStateWithLifecycle()
is a special collection method that takes the lifecycle of your composable into account. You need to add the dependency androidx.lifecycle:lifecycle-runtime-compose
to your gradle file.Applying these changes reduces your code and should fix the erratic flow behavior you experienced.
Upvotes: 0
Reputation: 478
Works after the following changes:
Return the combine instead of a new flow containing the combine
class Repository {
private val dao = Dao()
val item: Flow<String> = dao.dbItem2.combine(dao.dbItem) { value1, value2 ->
"$value1|$value2"
}
suspend fun increaseItem() = dao.increaseDbItem()
}
The issue with collectLatest was solved by changing from a flow to a callbackFlow:
val item: Flow<String> = callbackFlow {
dao.dbItem.collectLatest {
send(it)
}
awaitClose { }
}
Upvotes: 0