How can I get data when I convet a cold flow to hot flow using shareIn in Kotlin? - kotlin

I know that the Flow is cold, I can use collect to get every data just like Code A.
If I convert a Flow to hot flow using shareIn just like Code B, how can I get every data in a hot flow?
CodeA
val simple: Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}
class LatestNewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
simple.collect{ i- > print(i)}
}
}
}
}
Code B
val hotSimple: SharedFlow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
class LatestNewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
//I want to print every data in hotSimple.
}
}
}
}

The only way to guarantee it is to give shareIn a replay value that is at least as big as the number of values the source flow will produce. This wouldn’t be a practical solution in most cases because you don’t always know how many values the upstream flow will produce, or it might be infinite. Also, every item in the replay is kept in memory, so memory use might be a concern. It is only the emitted values that are replayed, so for new subscribers the side effects and delays will not be replayed along with the emitted values.
The whole point of using a SharedFlow is so it doesn’t have to restart for new subscribers. So typically you would not use a SharedFlow for something where you want every subscriber to receive every value from the beginning.

Related

Kotlin SharedFlow ViewModel emits before subscribed

I am trying to use SharedFlow as data provider for a Fragment in MVVM architecture.
In the Fragment class:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { value ->
handleData(data)
}
}
}
viewModel.init()
}
In the ViewModel class:
private val _data: MutableSharedFlow<DataState> = MutableSharedFlow()
val data: SharedFlow<DataState> = _data
fun init() {
...
//(listen for other data providers that generate data for SharedFlow)
...
viewModelCoroutineScope.launch {
val dataCollection = interactor.getDataCollection()
dataCollection.forEach { data ->
if (data != null) {
_data.emit(DataState(data = data))
}
}
}
}
The problem is that in 50% cases viewmodel.init() starts before subscriber under scope is connected to Flow - which results in some data lost.
Why SharedFlow is used? That is because ViewModel have subscriptions to other data sources which could send a lot of data instances in the irregular way all needed to collect, so StateFlow/LiveData with their "store only last value" is not good for this.
When I've tried to pin viewmodel.init() to subscriber coroutine like this:
val job = viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { value ->
handleData(data)
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
job.join()
viewModel.init()
}
the ViewModel emits data, but Fragment is never collects it.
What is right way to guarantee that subscribers is on before call of the ViewModel to start data sending through SharedFlow?
You should give your SharedFlow a replay value of 1 so late subscribers will still get the most recent value. You need this anyway. If the screen rotates, the recreated Fragment will need the latest value to show in the UI.
private val _data: MutableSharedFlow<DataState> = MutableSharedFlow(replay = 1)
But actually, it would be better to use shareIn instead of MutableSharedFlow, because then you can pause collection when there are no active subscribers, so you can avoid unnecessary monitoring of resources when the associated Fragment is off-screen. Like this:
val data: SharedFlow<DataState> = interactor.getDataCollection()
.mapNotNull { it?.let(::DataState) }
.shareIn(viewModelScope, SharingStarted.whileSubscribed(5000L), replay = 1)
If getDataCollection() is a suspend function, you could do it like this:
val data: SharedFlow<DataState> = flow {
interactor.getDataCollection().emitAll()
}
.mapNotNull { it?.let(::DataState) }
.shareIn(viewModelScope, SharingStarted.whileSubscribed(5000L), replay = 1)
If it's not a suspend function, why do you have a getter function at all? Kotlin uses properties instead.

Design pattern to best implement batch api requests that happen transparently to the calling layer

I have a batch processor that I want to refactor to be expressed a 1-to-1 fashion based on input to increase readability, and for further optimization later on. The issue is that there is a service that should be called in batches to reduce HTTP overhead, so mixing the 1-to-1 code with the batch code is a bit tricky, and we may not want to call the service with every input. Results can be sent out eagerly one-by-one, but order must be maintained, so something like a flow doesn't seem to work.
So, ideally the batch processor would look something like this:
class Processor<A, B> {
val service: Service<A, B>
val scope: CoroutineScope
fun processBatch(input: List<A>) {
input.map {
Pair(it, scope.async { service.call(it) })
}.map {
(a, b) ->
runBlocking { b.await().let { /** handle result, do something with a if result is null, etc **/ } }
}
}
}
The desire is to perform all of the service logic in such a way that it is executing in the background, automatically splitting the inputs for the service into batches, executing them asynchronously, and somehow mapping the result of the batch call into the suspended call.
Here is a hacky implementation:
class Service<A, B> {
val inputContainer: MutableList<A>
val outputs: MutableList<B>
val runCalled = AtomicBoolean(false)
val batchSize: Int
suspended fun call(input: A): B? {
// some prefiltering logic that returns a null early
val index = inputContainer.size
inputContainer.add(a) // add to overall list for later batching
return suspend {
run()
outputs[index]
}
}
fun run() {
val batchOutputs = mutableListOf<Deferred<List<B?>>>()
if (!runCalled.getAndSet(true)) {
inputs.chunked(batchSize).forEach {
batchOutputs.add(scope.async { batchCall(it) })
}
runBlocking {
batchOutputs.map {
val res = result.await()
outputs.addAll(res)
}
}
}
}
suspended fun batchCall(input: List<A>): List<B?> {
// batch API call, etc
}
}
Something like this could work but there are several concerns:
All API calls go out at once. Ideally this would be batching and executing in the background while other inputs are being scheduled, but this is not .
Processing of the service result for the first input cannot resume until all results have been returned. Ideally we could process the result if the service call has returned, while other results continue to be performed in the background.
Containers of intermediate results seem hacky and prone to bugs. Cleanup logic is also needed, which introduces more hacky bits into the rest of the code
I can think of several optimizations to the address 1 and 2, but I imagine concerns related to 3 would be worse. This seems like a fairly common call pattern and I would expect there to be a library or much simpler design pattern to accomplish this, but I haven't been able to find anything. Any guidance is appreciated.
You're on the right track by using Deferred. The solution I would use is:
When the caller makes a request, create a CompletableDeferred
Using a channel, pass this CompletableDeferred to the service for later completion
Have the caller suspend until the service completes the CompletableDeferred
It might look something like this:
val requestChannel = Channel<Pair<Request, CompletableDeferred<Result>>()
suspend fun doRequest(request: Request): Result {
val result = CompletableDeferred<Result>()
requestChannel.send(Pair(request, result))
return result.await()
}
fun run() = scope.launch {
while(isActive) {
val (requests, deferreds) = getBatch(batchSize).unzip()
val results = batchCall(requests)
(results zip deferreds).forEach { (result, deferred) ->
deferred.complete(result)
}
}
}
suspend fun getBatch(batchSize: Int) = buildList {
repeat(batchSize) {
add(requestChannel.receive())
}
}

Emit/Send Flow Values into BroadcastChannel

been pretty stuck on an issue with Kotlin flows/channels today. Essentially I want to take the values emitted from a flow, and immediately send them in a channel. We then subscribe to that channel as a flow via an exposed method. The use case here is to have a channel subscription that is always live and a flow that can be turned on and off independently.
private val dataChannel = BroadcastChannel<Data>(1)
suspend fun poll() {
poller.start(POLLING_PERIOD_MILLISECONDS)
.collect {
dataChannel.send(it)
}
}
suspend fun stopPoll() {
poller.stop()
}
suspend fun subscribe(): Flow<Data> {
return dataChannel.asFlow()
}
The simple use case I have here is a poller which returns a channelFlow. Ideally I could then emit to the channel in the collect method. This doesn't seem to work though. My rookie coroutine thought is that because collect and send are suspending, the emissions gets suspended in collect and we get stuck.
Is there any built in functions for flow or channel that can handle this or any other way to achieve this behavior?
For your case you can try to use hot stream of data SharedFlow instead of a Channel:
private val dataFlow = MutableSharedFlow<String>(extraBufferCapacity = 1)
suspend fun poll() {
poller.start(POLLING_PERIOD_MILLISECONDS)
.collect {
dataFlow.tryEmit(it)
}
}
suspend fun stopPoll() {
poller.stop()
}
fun subscribe(): Flow<Data> {
return dataFlow
}
tryEmit() - Tries to emit a value to this shared flow without suspending, so calling it will not suspend the collect block.

Kotlin Flow: Testing hangs

I am trying to test Kotlin implementation using Flows. I use Kotest for testing. This code works:
ViewModel:
val detectedFlow = flow<String> {
emit("123")
delay(10L)
emit("123")
}
Test:
class ScanViewModelTest : StringSpec({
"when the flow contains values they are emitted" {
val detectedString = "123"
val vm = ScanViewModel()
launch {
vm.detectedFlow.collect {
it shouldBe detectedString
}
}
}
})
However, in the real ViewModel I need to add values to the flow, so I use ConflatedBroadcastChannel as follows:
private val _detectedValues = ConflatedBroadcastChannel<String>()
val detectedFlow = _detectedValues.asFlow()
suspend fun sendDetectedValue(detectedString: String) {
_detectedValues.send(detectedString)
}
Then in the test I try:
"when the flow contains values they are emitted" {
val detectedString = "123"
val vm = ScanViewModel()
runBlocking {
vm.sendDetectedValue(detectedString)
}
runBlocking {
vm.detectedFlow.collect { it shouldBe detectedString }
}
}
The test just hangs and never completes. I tried all kind of things: launch or runBlockingTest instead of runBlocking, putting sending and collecting in the same or separate coroutines, offer instead of send... Nothing seems to fix it. What am I doing wrong?
Update: If I create flow manually it works:
private val _detectedValues = ConflatedBroadcastChannel<String>()
val detectedFlow = flow {
this.emit(_detectedValues.openSubscription().receive())
}
So, is it a bug in asFlow() method?
The problem is that the collect function you used in your test is a suspend function that will suspend the execution until the Flow is finished.
In the first example, your detectedFlow is finite. It will just emit two values and finish. In your question update, you are also creating a finite flow, that will emit a single value and finish. That is why your test works.
However, in the second (real-life) example the flow is created from a ConflatedBroadcastChannel that is never closed. Therefore the collect function suspends the execution forever. To make the test work without blocking the thread forever, you need to make the flow finite too. I usually use the first() operator for this. Another option is to close the ConflatedBroadcastChannel but this usually means modifications to your code just because of the test which is not a good practice.
This is how your test would work with the first() operator
"when the flow contains values they are emitted" {
val detectedString = "123"
val vm = ScanViewModel()
runBlocking {
vm.sendDetectedValue(detectedString)
}
runBlocking {
vm.detectedFlow.first() shouldBe detectedString
}
}

How to emit data to kotlin flow [duplicate]

I wanted to know how can I send/emit items to a Kotlin.Flow, so my use case is:
In the consumer/ViewModel/Presenter I can subscribe with the collect function:
fun observe() {
coroutineScope.launch {
// 1. Send event
reopsitory.observe().collect {
println(it)
}
}
}
But the issue is in the Repository side, with RxJava we could use a Behaviorsubject expose it as an Observable/Flowable and emit new items like this:
behaviourSubject.onNext(true)
But whenever I build a new flow:
flow {
}
I can only collect. How can I send values to a flow?
If you want to get the latest value on subscription/collection you should use a ConflatedBroadcastChannel:
private val channel = ConflatedBroadcastChannel<Boolean>()
This will replicate BehaviourSubject, to expose the channel as a Flow:
// Repository
fun observe() {
return channel.asFlow()
}
Now to send an event/value to that exposed Flow simple send to this channel.
// Repository
fun someLogicalOp() {
channel.send(false) // This gets sent to the ViewModel/Presenter and printed.
}
Console:
false
If you wish to only receive values after you start collecting you should use a BroadcastChannel instead.
To make it clear:
Behaves as an Rx's PublishedSubject
private val channel = BroadcastChannel<Boolean>(1)
fun broadcastChannelTest() {
// 1. Send event
channel.send(true)
// 2. Start collecting
channel
.asFlow()
.collect {
println(it)
}
// 3. Send another event
channel.send(false)
}
false
Only false gets printed as the first event was sent before collect { }.
Behaves as an Rx's BehaviourSubject
private val confChannel = ConflatedBroadcastChannel<Boolean>()
fun conflatedBroadcastChannelTest() {
// 1. Send event
confChannel.send(true)
// 2. Start collecting
confChannel
.asFlow()
.collect {
println(it)
}
// 3. Send another event
confChannel.send(false)
}
true
false
Both events are printed, you always get the latest value (if present).
Also, want to mention Kotlin's team development on DataFlow (name pending):
https://github.com/Kotlin/kotlinx.coroutines/pull/1354
Which seems better suited to this use case (as it will be a cold stream).
Take a look at MutableStateFlow documentation as it is a replacement for ConflatedBroadcastChannel that is going to be deprecated, very soon.
For a better context, look at the whole discussion on the original issue on Kotlin's repository on Github.
UPDATE:
Kotlin Coroutines 1.4.0 is now available with MutableSharedFlow, which replaces the need for Channel. MutableSharedFlow cleanup is also built in so you don't need to manually OPEN & CLOSE it, unlike Channel. Please use MutableSharedFlow if you need a Subject-like api for Flow
ORIGINAL ANSWER
Since your question had the android tag I'll add an Android implementation that allows you to easily create a BehaviorSubject or a PublishSubject that handles its own lifecycle.
This is relevant in Android because you don't want to forget to close the channel and leak memory. This implementation avoids the need to explicitly "dispose" of the reactive stream by tying it to the creation and destruction of the Fragment/Activity. Similar to LiveData
interface EventReceiver<Message> {
val eventFlow: Flow<Message>
}
interface EventSender<Message> {
fun postEvent(message: Message)
val initialMessage: Message?
}
class LifecycleEventSender<Message>(
lifecycle: Lifecycle,
private val coroutineScope: CoroutineScope,
private val channel: BroadcastChannel<Message>,
override val initialMessage: Message?
) : EventSender<Message>, LifecycleObserver {
init {
lifecycle.addObserver(this)
}
override fun postEvent(message: Message) {
if (!channel.isClosedForSend) {
coroutineScope.launch { channel.send(message) }
} else {
Log.e("LifecycleEventSender","Channel is closed. Cannot send message: $message")
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun create() {
channel.openSubscription()
initialMessage?.let { postEvent(it) }
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
channel.close()
}
}
class ChannelEventReceiver<Message>(channel: BroadcastChannel<Message>) :
EventReceiver<Message> {
override val eventFlow: Flow<Message> = channel.asFlow()
}
abstract class EventRelay<Message>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
channel: BroadcastChannel<Message>,
initialMessage: Message? = null
) : EventReceiver<Message> by ChannelEventReceiver<Message>(channel),
EventSender<Message> by LifecycleEventSender<Message>(
lifecycle,
coroutineScope,
channel,
initialMessage
)
By using the Lifecycle library from Android, I can now create a BehaviorSubject that cleans itself up after the activity/fragment has been destroyed
class BehaviorSubject<String>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
initialMessage = "Initial Message"
) : EventRelay<String>(
lifecycle,
coroutineScope,
ConflatedBroadcastChannel(),
initialMessage
)
or I can create a PublishSubject by using a buffered BroadcastChannel
class PublishSubject<String>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
initialMessage = "Initial Message"
) : EventRelay<String>(
lifecycle,
coroutineScope,
BroadcastChannel(Channel.BUFFERED),
initialMessage
)
And now I can do something like this
class MyActivity: Activity() {
val behaviorSubject = BehaviorSubject(
this#MyActivity.lifecycle,
this#MyActivity.lifecycleScope
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
behaviorSubject.eventFlow
.onEach { stringEvent ->
Log.d("BehaviorSubjectFlow", stringEvent)
// "BehaviorSubjectFlow: Initial Message"
// "BehaviorSubjectFlow: Next Message"
}
.flowOn(Dispatchers.Main)
.launchIn(this#MyActivity.lifecycleScope)
}
}
override fun onResume() {
super.onResume()
behaviorSubject.postEvent("Next Message")
}
}