How to create a flow with a few subscribtions in Kotlin? - kotlin

I need to run a task, which emits some data. I want to subscribe to this data like PublishSubject. But I can't solve a problem of one-instance flow. If I try to call it again, it will create another instance and the job will be done twice.
I tried to run the flow internally and post values to the BroadcastChannel, but this solution doesn't seem correct.
What is the best practice for such a task?

This will do the magic:
fun <T> Flow<T>.refCount(capacity: Int = Channel.CONFLATED, dispatcher: CoroutineDispatcher = Dispatchers.Default): Flow<T> {
class Context(var counter: Int) {
lateinit var job: Job
lateinit var channel: BroadcastChannel<T>
}
val context = Context(0)
fun lock() = synchronized(context) {
if (++context.counter > 1) {
return#synchronized
}
context.channel = BroadcastChannel(capacity)
context.job = GlobalScope.async(dispatcher) {
try {
collect { context.channel.offer(it) }
} catch (e: Exception) {
context.channel.close(e)
}
}
}
fun unlock() = synchronized(context) {
if (--context.counter == 0) {
context.job.cancel()
}
}
return flow {
lock()
try {
emitAll(context.channel.openSubscription())
} finally {
unlock()
}
}
}

Related

Can I use one Job instead of two Jobs when I use Flow in Kotlin?

In Code A, there are two Flows, and I assign two jobs for them, I collect the two Flows in fun beginSoundDensity() and stop collecting the two Flows in fun resetSoundDensity().
I think there are many repeated codes in Code A, so I hope to improve it, but Code B doesn't work.
Can I use one Job in my case?
Code A
private val _soundDensityState = MutableStateFlow(initialMSoundDensity)
val soundDensityState = _soundDensityState.asStateFlow()
private val _timeX = MutableStateFlow(0)
val timeX = _timeX.asStateFlow()
private var myJob1: Job?=null
private var myJob2: Job?=null
val myFlow: Flow<Int> = flow {
var i = 0
while (true) {
emit(i)
i = i + 15
delay(5000)
}
}
fun beginSoundDensity() {
myJob1?.cancel()
myJob2?.cancel()
myJob1 = viewModelScope.launch {
aSoundMeter.startSoundDensity {
pauseSoundDensity()
}.cancellable()
.collect {
_soundDensityState.value = it
}
}
myJob2 = viewModelScope.launch {
myFlow.collect {
_timeX.value = it
}
}
}
}
fun resetSoundDensity(){
myJob1?.cancel()
myJob2?.cancel()
}
Code B
//The same
private var myJob: Job?=null
val myFlow: Flow<Int> = flow {
var i = 0
while (true) {
emit(i)
i = i + 15
delay(5000)
}
}
fun beginSoundDensity() {
myJob?.cancel()
myJob = viewModelScope.launch {
aSoundMeter.startSoundDensity {
pauseSoundDensity()
}.cancellable()
.collect {
_soundDensityState.value = it
}
myFlow.collect {
_timeX.value = it //It will not be launched
}
}
}
}
fun resetSoundDensity(){
myJob?.cancel()
}
Yes and no. You need two separate coroutines running concurrently to collect from two flows. In your Code B myFlow will be collected only after aSoundMeter finishes collecting. Collections need to run at the same time, so you need two concurrent coroutines for this purpose.
However, if you always start and cancel both collections together, then I think it would be better to group them into a single coroutine like this:
fun beginSoundDensity() {
myJob?.cancel()
myJob = viewModelScope.launch {
coroutineScope {
launch {
aSoundMeter.startSoundDensity {
pauseSoundDensity()
}.cancellable()
.collect {
_soundDensityState.value = it
}
}
launch {
myFlow.collect {
_timeX.value = it //It will not be launched
}
}
}
}
}
fun resetSoundDensity(){
myJob?.cancel()
}

How to create a polling mechanism with kotlin coroutines?

I am trying to create a polling mechanism with kotlin coroutines using sharedFlow and want to stop when there are no subscribers and active when there is at least one subscriber. My question is, is sharedFlow the right choice in this scenario or should I use channel. I tried using channelFlow but I am unaware how to close the channel (not cancel the job) outside the block body. Can someone help? Here's the snippet.
fun poll(id: String) = channelFlow {
while (!isClosedForSend) {
try {
send(repository.getDetails(id))
delay(MIN_REFRESH_TIME_MS)
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
invokeOnClose { Timber.e("channel flow closed.") }
}
}
You can use SharedFlow which emits values in a broadcast fashion (won't emit new value until the previous one is consumed by all the collectors).
val sharedFlow = MutableSharedFlow<String>()
val scope = CoroutineScope(Job() + Dispatchers.IO)
var producer: Job()
scope.launch {
val producer = launch() {
sharedFlow.emit(...)
}
sharedFlow.subscriptionCount
.map {count -> count > 0}
.distinctUntilChanged()
.collect { isActive -> if (isActive) stopProducing() else startProducing()
}
fun CoroutineScope.startProducing() {
producer = launch() {
sharedFlow.emit(...)
}
}
fun stopProducing() {
producer.cancel()
}
First of all, when you call channelFlow(block), there is no need to close the channel manually. The channel will be closed automatically after the execution of block is done.
I think the "produce" coroutine builder function may be what you need. But unfortunately, it's still an experimental api.
fun poll(id: String) = someScope.produce {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val channel = poll("hello")
channel.receive()
channel.cancel()
}
The produce function will suspended when you don't call the returned channel's receive() method, so there is no need to delay.
UPDATE: Use broadcast for sharing values across multiple ReceiveChannel.
fun poll(id: String) = someScope.broadcast {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val broadcast = poll("hello")
val channel1 = broadcast.openSubscription()
val channel2 = broadcast.openSubscription()
channel1.receive()
channel2.receive()
broadcast.cancel()
}

How to subscribe to StateFlow in kotlin-react useEffect

I'm trying to create a small counter example for kotlin-react with functionalComponent with kotlin 1.4-M2.
The example should use kotlinx.coroutines.flow. I'm struggling at collecting the values from the store in reacts useEffect hook.
Store:
object CounterModel { // Modified sample from kotlin StateFlow doc
private val _counter = MutableStateFlow(0) // private mutable state flow
val counter: StateFlow<Int> get() = _counter // publicly exposed as read-only state flow
fun inc() { _counter.value++ }
}
Component:
val counter = functionalComponent<RProps> {
val (counterState, setCounter) = useState(CounterModel.counter.value)
useEffect(listOf()) {
// This does not work
GlobalScope.launch { CounterModel.counter.collect { setCounter(it) } }
}
div {
h1 {
+"Counter: $counterState"
}
button {
attrs.onClickFunction = { CounterModel.inc() }
}
}
}
When I directly call CounterModel.counter.collect { setCounter(it) } it complains about Suspend function 'collect' should be called only from a coroutine or another suspend function.
How would you implement this useEffect hook?
And once the subscription works, how would you unsubscribe from it (use useEffectWithCleanup instead of useEffect)?
Finally found a solution. We can use onEach to do an action for every new value and then 'subscribe' with launchIn. This returns a job that can be canceled for cleanup:
object CounterStore {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> get() = _counter
fun inc() { _counter.value++ }
}
val welcome = functionalComponent<RProps> {
val (counter, setCounter) = useState(CounterStore.counter.value)
useEffectWithCleanup(listOf()) {
val job = CounterStore.counter.onEach { setCounter(it) }.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
div {
+"Counter: $counter"
}
button {
attrs.onClickFunction = { CounterStore.inc() }
+"Increment"
}
}
We can extract this StateFlow logic to a custom react hook:
fun <T> useStateFlow(flow: StateFlow<T>): T {
val (state, setState) = useState(flow.value)
useEffectWithCleanup(listOf()) {
val job = flow.onEach { setState(it) }.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
return state
}
And use it like this in our component:
val counter = useStateFlow(CounterStore.counter)
The complete project can be found here.
The Flow-Api is very experimental so this might not be the final solution :)
if's very important to check that the value hasn't changed,
before calling setState, otherwise the rendering happens twice
external interface ViewModelProps : RProps {
var viewModel : MyViewModel
}
val App = functionalComponent<ViewModelProps> { props ->
val model = props.viewModel
val (state, setState) = useState(model.stateFlow.value)
useEffectWithCleanup {
val job = model.stateFlow.onEach {
if (it != state) {
setState(it)
}
}.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
}

Kotlin async processing with optional async dependencies

I have a function that conditionally fetches some data and runs some tasks concurrently on that data. Each task depends on different sets of data and I would like to avoid fetching the data that's not needed. Moreover, some of the data can have already been prefetched and given to the function. See the code I've come up with below.
suspend fun process(input: SomeInput, prefetchedDataX: DataX?, prefetchedDataY: DataY?) = coroutineScope {
val dataXAsync = lazy {
if (prefetchedDataX == null) {
async { fetchDataX(input) }
} else CompletableDeferred(prefetchedDataX)
}
val dataYAsync = lazy {
if (prefetchedDataY == null) {
async { fetchDataY(input) }
} else CompletableDeferred(prefetchedDataY)
}
if (shouldDoOne(input)) launch {
val (dataX, dataY) = awaitAll(dataXAsync.value, dataYAsync.value)
val modifiedDataX = modifyX(dataX)
val modifiedDataY = modifyY(dataY)
doOne(modifiedDataX, modifiedDataY)
}
if (shouldDoTwo(input)) launch {
val modifiedDataX = modifyX(dataXAsync.value.await())
doTwo(modifiedDataX)
}
if (shouldDoThree(input)) launch {
val modifiedDataY = modifyY(dataYAsync.value.await())
doThree(modifiedDataY)
}
}
Any improvements that could be made to this code? One, I don't like having to fakely wrap the prefetched data into a CompletableDeferred. Two, I don't like having to call modifyX, modifyY inside each task, I wish I could apply it at the fetching stage, but I haven't come up with a nice way to do that. Alternatively I could do
val modifiedDataXAsync = lazy {
async { modifyX(prefetchedDataX ?: fetchDataX(input)) }
}
but it feels wasteful to be spawning a new coroutine when the data is already prefetched. Am I over-optimizing?
How about this? This code is pretty similar to yours, I just simplified it a bit.
suspend fun process(input: SomeInput, prefetchedDataX: DataX?, prefetchedDataY: DataY?) = coroutineScope {
val modifiedDataX by lazy {
async { modifyX(prefetchedDataX ?: fetchDataX(input)) }
}
val modifiedDataY by lazy {
async { modifyY(prefetchedDataY ?: fetchDataY(input)) }
}
if (shouldDoOne(input)) launch {
val (dataX, dataY) = awaitAll(modifiedDataX, modifiedDataY)
doOne(dataX, dataY)
}
if (shouldDoTwo(input)) launch {
doTwo(modifiedDataX.await())
}
if (shouldDoThree(input)) launch {
doThree(modifiedDataY.await())
}
}

Returning a value produced in Kotlin coroutine

I am trying to return a value generated from coroutine
fun nonSuspending (): MyType {
launch(CommonPool) {
suspendingFunctionThatReturnsMyValue()
}
//Do something to get the value out of coroutine context
return somehowGetMyValue
}
I have come up with the following solution (not very safe!):
fun nonSuspending (): MyType {
val deferred = async(CommonPool) {
suspendingFunctionThatReturnsMyValue()
}
while (deferred.isActive) Thread.sleep(1)
return deferred.getCompleted()
}
I also thought about using event bus, but is there a more elegant solution to this problem?
Thanks in advance.
You can do
val result = runBlocking(CommonPool) {
suspendingFunctionThatReturnsMyValue()
}
to block until the result is available.
You can use this:
private val uiContext: CoroutineContext = UI
private val bgContext: CoroutineContext = CommonPool
private fun loadData() = launch(uiContext) {
try {
val task = async(bgContext){dataProvider.loadData("task")}
val result = task.await() //This is the data result
}
}catch (e: UnsupportedOperationException) {
e.printStackTrace()
}
}