Colton Idle
08/16/2022, 12:43 AMval myFlow = flow { while (true) {
delay(1000)
emit(1) } }
VM
var myState by mutableStateOf(0)
init {
viewModelScope.launch {
myFlow.collect {
myState = myState.plus(it)
}
}
}
Composable
Text(text = vm.myState.toString())
ephemient
08/16/2022, 12:52 AMLifecycle.repeatOnLifecycle
cancels and restarts the block, so it upstream is also cancelled while your app is not in the expected lifecycle stateColton Idle
08/16/2022, 1:05 AMIan Lake
08/16/2022, 3:17 AMFrancesc
08/16/2022, 3:32 AMOleksandr Balan
08/16/2022, 6:44 AMState
from the VM is a "bad" thing? Because in such way Composable cannot "unsubsribe" from it?Stylianos Gakis
08/16/2022, 8:17 AMinit
block and expose it as something that the UI can subscribe to
class MyViewModel(
val myFlow: Flow<Int> = flow {
while (currentCoroutineContext().isActive) {
delay(1000)
emit(1)
}
},
) : ViewModel() {
var myState by mutableStateOf(0)
fun subscribeToFirestoreChanges(): Flow<Unit> {
return flow {
myFlow.collect {
myState = myState.plus(it)
}
}
}
}
Then on the UI simply turn the subscription on with the safe repeatOnLifecycle
and decide on which state you want it to be on from (in this case Lifecycle.State.STARTED
)
fun MyComposable(viewModel: MyViewModel) {
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(viewModel, lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.subscribeToFirestoreChanges().collect()
}
}
val text = viewModel.myState
}
Not sure if there are any problems with such an approach. I’ve done it once here and here since my subscribeToAuthResultUseCase
was fetching for a status update every 1 second and only wanted that to happen while on screen. And this worked just fine for this use case👌Colton Idle
08/16/2022, 3:03 PMIf you want collection to only be happening when your UI is present, then collecting in the ViewModel was always the wrong thing to be doingHm. Then I probably never want to be calling collect{} in a VM as I can't think of any reason when I want to be collecting something even when I'm not on the screen. I wonder if I can write a lint rule to prevent me from making this mistake.
Francesc
08/16/2022, 3:11 PMcollect
in the viewmodel, the business logic belongs there and the UI layer should not be talking to your data layer. What needs to happen is for your viewmodel to be aware when the UI is no longer collecting the state so that it can unsubscribe its own collectorsColton Idle
08/16/2022, 3:33 PMFrancesc
08/16/2022, 3:38 PMstateIn
in your viewmodel to expose the UI state, and have it active only while subscribed, with an optional timeout (for configuration changes, for instance), so that it has a grace period before it disconnectscollectWithLifecycle
(or whatever it's called) does, so you don't need to do the repeat on lifecycle yourselfStylianos Gakis
08/16/2022, 3:41 PMstateIn
would work with a Flow
not with State<T>
which I guess is not what Colton is going for.
The new collectStateWithLifecycle
API works when collecting a Flow not when you got compose State in your ViewModel afaik. I was trying to help for this specific case. Personally I also try to expose everything as a Flow anyway with stateIn
in order to get this functionality for free. But it’s true that I haven’t managed to get that working for all the cases I’ve encountered.Francesc
08/16/2022, 3:45 PMDisposableEffect(key1 = viewModel) {
viewModel.onStart()
onDispose { viewModel.onStop() }
}
this is a more manual approach but it would allow you to start/stop the collection when the composable enters/exits the compositioninit
block to onStart
and have an onStop
where you stop your flows (you would get the returned job
and cancel()
that)Colton Idle
08/16/2022, 3:46 PMFrancesc
08/16/2022, 3:47 PMColton Idle
08/16/2022, 3:47 PMStylianos Gakis
08/16/2022, 6:51 PMColton Idle
08/16/2022, 6:57 PMvar myState by mutableStateOf(0)
val myFlow = flow {
while (true) {
delay(1000)
emit(1)
}
}
fun subscribeToFlow1(): Flow<Unit> = flow {
myFlow.collectLatest {
myState = myState.plus(it)
}
}
Composable screen: real easy!
vm.subscribeToFlow1().collectAsStateWithLifecycle(initialValue = Unit)
Essentially all I'm doing is wrapping my current flows in another flow and then moving them out of init {} and then calling collect in the composable screen.Francesc
08/16/2022, 7:26 PMstateIn
to your myFlow
? that would achieve the same result without the wrapper flow if you used "while subscribed"Colton Idle
08/16/2022, 7:27 PMFrancesc
08/16/2022, 7:27 PMonEach { myState = myState.plus(it) }
Colton Idle
08/16/2022, 7:30 PMstateIn
to your myFlow
?"fun subscribeToFlow1(): Flow<Unit> = flow<Unit> {
myFlow.collectLatest {
myState = myState.plus(it)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Unit)
I'm not sure how I'd get rid of the wrapper flow {}ephemient
08/16/2022, 7:54 PMColton Idle
08/16/2022, 7:57 PMIan Lake
08/16/2022, 7:58 PMFrancesc
08/16/2022, 7:59 PMval myFlow = flow {
while (true) {
delay(1000)
emit(1)
}
}
.onEach { /* TODO */ }
.stateIn(...)
Colton Idle
08/16/2022, 8:00 PMFrancesc
08/16/2022, 8:02 PMonEach
is another way to collect your flow, when you use launchIn
ephemient
08/16/2022, 8:03 PMscope.launch { flow.collect { ... } }
== flow.onEach { ... }.launchIn(scope)
to be clearColton Idle
08/16/2022, 8:04 PMephemient
08/16/2022, 8:05 PM.collect { ... }
== .onEach { ... }.collect()
and scope.launch { .collect() }
== .launchIn(scope)
, but you almost never see the parameterless .collect()
Colton Idle
08/16/2022, 8:10 PMvar myState by mutableStateOf(0)
val myFlow = flow {
while (true) {
delay(1000)
emit(1)
}
}.onEach { myState = myState.plus(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Unit)
Composable screen:
vm.myFlow.collectAsStateWithLifecycle(initialValue = Unit)
The only thing that I'm not in absolute love with is the fact that I have two initial values declared, once as the 3rd arg stateIn and another as the arg in collectAsStateWithLifecycle. No wrapper flows or anything!init{
viewModelScope.launch {
userRepo.getUser().collectLatest { user ->
apiService.getBooksForUser(user.id).collect {
...
I'll try working on the above though. maybe theres some collectLatest equivalent with onEachFrancesc
08/16/2022, 8:29 PMStateFlow
, you don't need to define another init value in your composable if you expose it as a StateFlow
userRepo.getUser().collectLatest { user ->
apiService.getBooksForUser(user.id).collect {
use flatmapLatest
instead
userRepo.getUser().flatmapLatest { user -> apiService.getBooksForuser(user.id) }
Colton Idle
08/16/2022, 8:33 PMyour VM flow is aoh. neat, you don't need to define another init value in your composable if you expose it as aStateFlow
StateFlow
useawesome. theres a method for everything!insteadflatmapLatest
userRepo.getUser().flatmapLatest { user -> apiService.getBooksForuser(user.id) }
didn't work
but
userRepo.getUser().mapLatest { user -> apiService.getBooksForuser(user.id) }
didFrancesc
08/17/2022, 5:55 AMColton Idle
08/17/2022, 2:39 PM