How to pause emitting Flow in Kotlin? - android

Suppose I have some data that I need to transfer to the UI, and the data should be emitted with a certain delay, so I have a Flow in my ViewModel:
val myFlow = flow {
listOfSomeData.forEachIndexed { index, data ->
//....
emit(data.UIdata)
delay(data.requiredDelay)
}
}
Somewhere in the UI flow is collected and displayed:
#Composable
fun MyUI(viewModel: ViewModel) {
val data by viewModel.myFlow.collectAsState(INITIAL_DATA)
//....
}
Now I want the user to be able to pause/resume emission by pressing some button. How can i do this?
The only thing I could come up with is an infinite loop inside Flow builder:
val pause = mutableStateOf(false)
//....
val myFlow = flow {
listOfSomeData.forEachIndexed { index, data ->
emit(data.UIdata)
delay(data.requiredDelay)
while (pause.value) { delay(100) } //looks ugly
}
}
Is there any other more appropriate way?

You can tidy up your approach by using a flow to hold pause value then collect it:
val pause = MutableStateFlow(false)
//....
val myFlow = flow {
listOfSomeData.forEachIndexed { index, data ->
emit(data.UIdata)
delay(data.requiredDelay)
if (pause.value) pause.first { isPaused -> !isPaused } // suspends
}
}
Do you need mutableStateOf for compose? Maybe you can transform it into a flow but I'm not aware how it looks bc I don't use compose.
A bit of a creative rant below:
I actually was wondering about this and looking for more flexible approach - ideally source flow should suspend during emit. I noticed that it can be done when using buffered flow with BufferOverflow.SUSPEND so I started fiddling with it.
I came up with something like this that lets me suspend any producer:
// assume source flow can't be accessed
val sourceFlow = flow {
listOfSomeData.forEachIndexed { index, data ->
emit(data.UIdata)
delay(data.requiredDelay)
}
}
val pause = MutableStateFlow(false)
val myFlow = sourceFlow
.buffer(Channel.RENDEZVOUS, BufferOverflow.SUSPEND)
.transform {
if (pause.value) pause.first { isPaused -> !isPaused }
emit(it)
}
.buffer()
It does seem like a small hack to me and there's a downside that source flow will still get to the next emit call after pausing so: n value gets suspended inside transform but source gets suspended on n+1.
If anyone has better idea on how to suspend source flow "immediately" I'd be happy to hear it.

If you don't need a specific delay you can use flow.filter{pause.value != true}

Related

Kotlin Flow not collected anymore after working initially

Basically I want to make a network request when initiated by the user, collect the Flow returned by the repository and run some code depending on the result. My current setup looks like this:
Viewmodel
private val _requestResult = MutableSharedFlow<Result<Data>>()
val requestResult = _requestResult.filterNotNull().shareIn(
scope = viewModelScope,
started = SharingStarted.WhileViewSubscribed,
replay = 0
)
fun makeRequest() {
viewModelScope.launch {
repository.makeRequest().collect { _requestResult.emit(it) }
}
}
Fragment
buttonLayout.listener = object : BottomButtonLayout.Listener {
override fun onButtonClick() {
viewModel.makeRequest()
}
}
lifecycleScope.launchWhenCreated {
viewModel.requestResult.collect { result ->
when (result) {
Result.Loading -> {
doStuff()
}
is Result.Success -> {
doDifferentStuff(result.data)
}
is Result.Failure -> {
handleError()
}
}
}
}
The first time the request is made everything seems to work. But starting with the second time the collect block in the fragment does not run anymore. The request is still made, the repository returns the flow as expected, the collect block in the viewmodel runs and emit() also seems to be executed successfully.
So what could be the problem here? Something about the coroutine scopes? Admittedly I lack any sort of deeper understanding of the matter at hand.
Also is there a more efficient way of accomplishing what I'm attempting using Kotlin Flows in general? Collecting a flow and then emitting the same flow again seems a bit counterintuitive.
Thanks in advance:)
According to the documentation there are two recommended alternatives:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
//your thing
}
}
I rather the other alternative:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.makeReques().flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
I like the flowWithLifecycle shorter syntax and less boiler plate. Be carefull thar is bloking so you cant have anything after that.
The oficial docs
https://developer.android.com/topic/libraries/architecture/coroutines
Please be aware you need the lifecycle aware library.

Updating MutableStateFlow without emitting to collectors

In an Android project, we are currently trying to switch from LiveData to StateFlow in our viewmodels. But for some rare cases, we need to update our state without notifying the collectors about the change. It might sound weird when we think of the working mechanism of flows, but I want to learn if it's a doable thing or not. Any real solution or workaround would be appreciated.
If you don't need to react to the true state anywhere, but only the publicly emitted state, I would store the true state in a property directly instead of a MutableStateFlow.
private var trueState: MyState = MyState(someDefault)
private val _publicState = MutableStateFlow<MyState>()
val publicstate = _publicState.asStateFlow()
fun updateState(newState: MyState, shouldEmitPublicly: Boolean) {
trueState = newState
if (shouldEmitPublicly) {
_publicState.value = newState
}
}
If you do need to react to it, one alternative to a wrapper class and filtering (#broot's solution) would be to simply keep two separate StateFlows.
Instead of exposing the state flow directly, we can expose another flow that filters the items according to our needs.
For example, we can keep the shouldEmit flag inside emitted items. Or use any other filtering logic:
suspend fun main(): Unit = coroutineScope {
launch {
stateFlow.collect {
println("Collected: $it")
}
}
delay(100)
setState(1)
delay(100)
setState(2)
delay(100)
setState(3, shouldEmit = false)
delay(100)
setState(4)
delay(100)
setState(5)
delay(100)
}
private val _stateFlow = MutableStateFlow(EmittableValue(0))
val stateFlow = _stateFlow.filter { it.shouldEmit }
.map { it.value }
fun setState(value: Int, shouldEmit: Boolean = true) {
_stateFlow.value = EmittableValue(value, shouldEmit)
}
private data class EmittableValue<T>(
val value: T,
val shouldEmit: Boolean = true
)
We can also keep the shouldEmit flag in the object and switch it on/off to temporarily disable emissions.
If you need to expose StateFlow and not just Flow, this should also be possible, but you need to decide if ignored emissions should affect its value or not.

How to combine livedata and kotlin flow

Is this good to put the collect latest inside observe?
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!)
.observe(viewLifecycleOwner) {
if (it != null) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.referralDetailsResponse.collect { referralResponseState ->
when (referralResponseState) {
State.Empty -> {
}
is State.Failed -> {
Timber.e("${referralResponseState.message}")
}
State.Loading -> {
Timber.i("LOADING")
}
is State.Success<*> -> {
// ACCESS LIVEDATA RESULT HERE??
}}}}
I'm sure it isn't, my API is called thrice too as the local DB changes, what is the right way to do this?
My ViewModel looks like this where I'm getting user information from local Room DB and referral details response is the API response
private val _referralDetailsResponse = Channel<State>(Channel.BUFFERED)
val referralDetailsResponse = _referralDetailsResponse.receiveAsFlow()
init {
val inviteSlug: String? = savedStateHandle["inviteSlug"]
// Fire invite link
if (inviteSlug != null) {
referralDetail(inviteSlug)
}
}
fun referralDetail(referral: String?) = viewModelScope.launch {
_referralDetailsResponse.send(State.Loading)
when (
val response =
groupsRepositoryImpl.referralDetails(referral)
) {
is ResultWrapper.GenericError -> {
_referralDetailsResponse.send(State.Failed(response.error?.error))
}
ResultWrapper.NetworkError -> {
_referralDetailsResponse.send(State.Failed("Network Error"))
}
is ResultWrapper.Success<*> -> {
_referralDetailsResponse.send(State.Success(response.value))
}
}
}
fun fetchUserProfileLocal(username: String) =
userRepository.getUserLocal(username).asLiveData()
You can combine both streams of data into one stream and use their results. For example we can convert LiveData to Flow, using LiveData.asFlow() extension function, and combine both flows:
combine(
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!).asFlow(),
viewModel.referralDetailsResponse
) { userProfile, referralResponseState ->
...
}.launchIn(viewLifecycleOwner.lifecycleScope)
But it is better to move combining logic to ViewModel class and observe the overall result.
Dependency to use LiveData.asFlow() extension function:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
it certainly is not a good practice to put a collect inside the observe.
I think what you should do is collect your livedata/flows inside your viewmodel and expose the 'state' of your UI from it with different values or a combined state object using either Flows or Livedata
for example in your first code block I would change it like this
get rid of "userProfile" from your viewmodel
create and expose from your viewmodel to your activity three LiveData/StateFlow objects for your communityFeedPageData, errorMessage, refreshingState
then in your viewmodel, where you would update the "userProfile" update the three new state objects instead
this way you will take the business logic of "what to do in each state" outside from your activity and inside your viewmodel, and your Activity's job will become to only update your UI based on values from your viewmodel
For the specific case of your errorMessage and because you want to show it only once and not re-show it on Activity rotation, consider exposing a hot flow like this:
private val errorMessageChannel = Channel<CharSequence>()
val errorMessageFlow = errorMessageChannel.receiveAsFlow()
What "receiveAsFlow()" does really nicely, is that something emitted to the channel will be collected by one collector only, so a new collector (eg if your activity recreates on a rotation) will not receive the message and your user will not see it again

How to pause/stop collecting/emitting data in a Flow while app minimised?

I have a UseCase and remote repository that return Flow in a loop and I collect the result of UseCase in the ViewModel like this:
viewModelScope.launch {
useCase.updatePeriodically().collect { result ->
when (result.status) {
Result.Status.ERROR -> {
errorModel.value = result.errorModel
}
Result.Status.SUCCESS -> {
items.value = result.data
}
Result.Status.LOADING -> {
loading.value = true
}
}
}
}
the problem is when the app is in the background (minimized) flow continues working. so can I pause it when the app is in the background and resume it when the app comes back to the foreground?
and also I don't want to observe the data in my view (fragment or activity).
I'd play around with the stateIn operator and the way I'm currently consuming the flow in the view.
Something like:
val state = useCase.updatePeriodically().map { ... }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed, initialValue)
And consume it from the View like:
viewModel.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
}
.launchIn(lifecycleScope)
For other potential ways on how to collect flows from the UI: https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
EDIT:
If you don't want to consume it from the view, you still have to signal for the VM that your View is in the background currently.
Something like:
private var job: Job? = null
fun start(){
job = viewModelScope.launch {
state.collect { ... }
}
}
fun stop(){
job?.cancel()
}
Even if the viewModelScope is cancelled, the flow will continue to collect because it is not cooperative to cancellation.
To make a flow cancellable, you can do one of the following things:
In the collect lambda, call currentCoroutineContext().ensureActive() to make sure the context in which the flow is being collected is still active. This will however throw a CancellableException, which you will need to catch, if the coroutine scope was cancelled already (viewModel scope for your case.)
You can use cancellable() operator as follows:
myFlow.cancellable().collect { //do stuff here.. }
And you can call cancel() whenever you want to cancel the flow.
For official documentation on cancelling the flow see:
https://kotlinlang.org/docs/flow.html#flow-cancellation-checks
I believe you want something like this
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
state.collect {
}
}
}
Here's an execellent article on repeatOnLifecyle: https://medium.com/androiddevelopers/repeatonlifecycle-api-design-story-8670d1a7d333

PublishSubject with Kotlin coroutines (Flow)

I used a PublishSubject and I was sending messages to it and also I was listening for results. It worked flawlessly, but now I'm not sure how to do the same thing with Kotlin's coroutines (flows or channels).
private val subject = PublishProcessor.create<Boolean>>()
...
fun someMethod(b: Boolean) {
subject.onNext(b)
}
fun observe() {
subject.debounce(500, TimeUnit.MILLISECONDS)
.subscribe { /* value received */ }
}
Since I need the debounce operator I really wanted to do the same thing with flows so I created a channel and then I tried to create a flow from that channel and listen to changes, but I'm not getting any results.
private val channel = Channel<Boolean>()
...
fun someMethod(b: Boolean) {
channel.send(b)
}
fun observe() {
flow {
channel.consumeEach { value ->
emit(value)
}
}.debounce(500, TimeUnit.MILLISECONDS)
.onEach {
// value received
}
}
What is wrong?
Flow is a cold asynchronous stream, just like an Observable.
All transformations on the flow, such as map and filter do not trigger flow collection or execution, only terminal operators (e.g. single) do trigger it.
The onEach method is just a transformation. Therefore you should replace it with the terminal flow operator collect. Also you could use a BroadcastChannel to have cleaner code:
private val channel = BroadcastChannel<Boolean>(1)
suspend fun someMethod(b: Boolean) {
channel.send(b)
}
suspend fun observe() {
channel
.asFlow()
.debounce(500)
.collect {
// value received
}
}
Update: At the time the question was asked there was an overload of debounce with two parameters (like in the question). There is not anymore. But now there is one which takes one argument in milliseconds (Long).
It should be SharedFlow/MutableSharedFlow for PublishProcessor/PublishRelay
private val _myFlow = MutableSharedFlow<Boolean>(
replay = 0,
extraBufferCapacity = 1, // you can increase
BufferOverflow.DROP_OLDEST
)
val myFlow = _myFlow.asSharedFlow()
// ...
fun someMethod(b: Boolean) {
_myFlow.tryEmit(b)
}
fun observe() {
myFlow.debounce(500)
.onEach { }
// flowOn(), catch{}
.launchIn(coroutineScope)
}
And StateFlow/MutableStateFlow for BehaviorProcessor/BehaviorRelay.
private val _myFlow = MutableStateFlow<Boolean>(false)
val myFlow = _myFlow.asStateFlow()
// ...
fun someMethod(b: Boolean) {
_myFlow.value = b // same as _myFlow.emit(v), myFlow.tryEmit(b)
}
fun observe() {
myFlow.debounce(500)
.onEach { }
// flowOn(), catch{}
.launchIn(coroutineScope)
}
StateFlow must have initial value, if you don't want that, this is workaround:
private val _myFlow = MutableStateFlow<Boolean?>(null)
val myFlow = _myFlow.asStateFlow()
.filterNotNull()
MutableStateFlow uses .equals comparison when setting new value, so it does not emit same value again and again (versus distinctUntilChanged which uses referential comparison).
So MutableStateFlow ≈ BehaviorProcessor.distinctUntilChanged(). If you want exact BehaviorProcessor behavior then you can use this:
private val _myFlow = MutableSharedFlow<Boolean>(
replay = 1,
extraBufferCapacity = 0,
BufferOverflow.DROP_OLDEST
)
ArrayBroadcastChannel in Kotlin coroutines is the one most similar to PublishSubject.
Like PublishSubject, an ArrayBroadcastChannel can have multiple
subscribers and all the active subscribers are immediately notified.
Like PublishSubject, events pushed to this channel are lost, if there are no active subscribers at the moment.
Unlike PublishSubject, backpressure is inbuilt into the coroutine channels, and that is where the buffer capacity comes in. This number really depends on which use case the channel is being used for. For most of the normal use cases, I just go with 10, which should be more than enough. If you push events faster to this channel than receivers consuming it, you can give more capacity.
Actually BroadcastChannel is obsolete already, Jetbrains changed their approach to use SharedFlows instead. Which is a lot more cleaner, easier to implement and solves a lot of pain points.
Essentially, you can achieve the same thing like this.
class BroadcastEventBus {
private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow() // read-only public view
suspend fun postEvent(event: Event) {
_events.emit(event) // suspends until subscribers receive it
}
}
To read about it more, checkout Roman's Medium article.
"Shared flows, broadcast channels" by Roman Elizarov

Categories

Resources