Removing variable mutation from reactive stream processing - kotlin

The following works, but I think it can be better i.e. more idiomatic rx/functional/less mutable.
var total = 0
var published = 0
service
.getThings()
.doOnNext { total++ }
.flatMap { shouldPublish(it) } // Returns empty() or just(it)
.doOnNext { published++ }
.subscribe { publishToSqs(it) }
println("Published $publish out of $total")
Any ideas?

I normally use '.scan()' operator upon a '.merge()' d stream of events. That way there is always a valid snapshot of the latest state.
More about it here - Reactive Aggregate State Pipeline

Related

Kotlin flows SharedFlow not received in collectInLifeCycle inside Fragment

I am observing inside a fragment the events of a sharedflow such as this:
myEvent.collectInLifeCycle(viewLifecycleOwner) { event ->
when (state) {
//check the event. The event emited form onStart is never reached here :(
}
}
Whereas in the viewmodel I have
private val _myEvent = MutableSharedFlow<MyEvent>()
val myEvent: SharedFlow<MyEvent> = _myEvent
fun loadData() =
viewModelScope.launch {
getDataUseCase
.safePrepare(onGenericError = { _event.emit(Event.Error(null)) })
.onStart { _event.emit(Event.Loading) }
.onEach { result ->
result.onSuccess { response ->
_event.emit(Event.Something)
}
}
.launchIn(viewModelScope)
}
So the problem is that only the Event.Something is the one being properly collected from the fragment, whereas _event.emit(Event.Loading) is not being collected... If I debug it goes to the onStart, but it is never called in the fragment.
Your SharedFlow needs to have a replay so that collectors always get at least the most recent value. Otherwise, if you emit to the Flow before the collector is registered, it will never see anything emitted. Do this:
private val _myEvent = MutableSharedFlow<MyEvent>(replay = 1)
Personally, unless I'm missing some detail here that would change my mind, I would simplify all your code to avoid having to manually call loadData(). Something like this but I'm guessing a bit because I don't know all your types and functions.
val myEvent: SharedFlow<MyEvent> = flow {
emit(Event.Loading)
emitAll(
getDataUseCase
.transform { result ->
result.onSuccess { response ->
emit(Event.Something)
}
}
.catch { error -> emit(Event.Error(null)) }
)
}.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1)

Why does my Kotlin Flow onCompletion never run?

So I have a flow where I need it to emit a value from cache, but at the end it will make an API call to pull values in case there was nothing in cache (or refresh the value it has). I am trying this
override val data: Flow<List<Data>> = dataDao.getAllCachedData()
.onCompletion {
coroutineScope {
launch {
requestAndCacheDataOrEmitError()
}
}
}
.map { entities ->
entities
.map { it.toData() }
.filter { it !is Data.Unknown }
}
.filterNotNull()
.catch { emitRepositoryError(it) }
So the idea is that we emit the cache, and then make an API call to fetch new data regardless of the original mapping. But I do not want it blocking. For example, if we use this flow, I do not ever want the calling function to be blocked by the onCompletion.
I think the problem is that the onCompletion never runs. I set some breakpoints/logs and it never runs at all, even outside of the coroutineScope.
I don't quite understand the work you are doing but I think when you are collecting flow on a certain scope. You end the scope that flow will be put into onCompletion
var job : Job? = null
fun scan() {
job = viewModelScope.launch {
bigfileManager.bigFile.collect {
if (it is ResultOrProgress.Result) {
_bigFiles.value = it.result ?: emptyList()
} else {
_updateProgress.value = (it as ResultOrProgress.Progress).progress ?: 0
}
}
}
}
fun endScreen(){
job?.cancel()
}

Kotlin Flow: How to unsubscribe/stop

Update Coroutines 1.3.0-RC
Working version:
#FlowPreview
suspend fun streamTest(): Flow<String> = channelFlow {
listener.onSomeResult { result ->
if (!isClosedForSend) {
offer(result)
}
}
awaitClose {
listener.unsubscribe()
}
}
Also checkout this Medium article by Roman Elizarov: Callbacks and Kotlin Flows
Original Question
I have a Flow emitting multiple Strings:
#FlowPreview
suspend fun streamTest(): Flow<String> = flowViaChannel { channel ->
listener.onSomeResult { result ->
if (!channel.isClosedForSend) {
channel.sendBlocking(result)
}
}
}
After some time I want to unsubscribe from the stream. Currently I do the following:
viewModelScope.launch {
beaconService.streamTest().collect {
Timber.i("stream value $it")
if(it == "someString")
// Here the coroutine gets canceled, but streamTest is still executed
this.cancel()
}
}
If the coroutine gets canceled, the stream is still executed. There is just no subscriber listening to new values. How can I unsubscribe and stop the stream function?
A solution is not to cancel the flow, but the scope it's launched in.
val job = scope.launch { flow.cancellable().collect { } }
job.cancel()
NOTE: You should call cancellable() before collect if you want your collector stop when Job is canceled.
You could use the takeWhile operator on Flow.
flow.takeWhile { it != "someString" }.collect { emittedValue ->
//Do stuff until predicate is false
}
For those willing to unsubscribe from the Flow within the Coroutine scope itself, this approach worked for me :
viewModelScope.launch {
beaconService.streamTest().collect {
//Do something then
this.coroutineContext.job.cancel()
}
}
With the current version of coroutines / Flows (1.2.x) I don't now a good solution. With onCompletion you will get informed when the flow stops, but you are then outside of the streamTest function and it will be hard to stop listening of new events.
beaconService.streamTest().onCompletion {
}.collect {
...
}
With the next version of coroutines (1.3.x) it will be really easy. The function flowViaChannel is deprecated in favor for channelFlow. This function allows you to wait for closing of the flow and do something in this moment, eg. remove listener:
channelFlow<String> {
println("Subscribe to listener")
awaitClose {
println("Unsubscribe from listener")
}
}
When a flow runs in couroutin scope, you can get a job from it to controls stop subscribe.
// Make member variable if you want.
var jobForCancel : Job? = null
// Begin collecting
jobForCancel = viewModelScope.launch {
beaconService.streamTest().collect {
Timber.i("stream value $it")
if(it == "someString")
// Here the coroutine gets canceled, but streamTest is still executed
// this.cancel() // Don't
}
}
// Call whenever to canceled
jobForCancel?.cancel()
For completeness, there is a newer version of the accepted answer. Instead of explicitly using the launch coroutine builder, we can use the launchIn method directly on the flow:
val job = flow.cancellable().launchIn(scope)
job.cancel()
Based on #Ronald answer this works great for testing when you need to make your Flow emits again.
val flow = MutableStateFlow(initialValue)
flow.take(n).collectIndexed { index, _ ->
if (index == something) {
flow.value = update
}
}
//your assertions
We have to know how many emissions in total we expect n and then we can use the index to know when to update the Flow so we can receive more emissions.
If you want to cancel only the subscription being inside it, you can do it like this:
viewModelScope.launch {
testScope.collect {
return#collect cancel()
}
}
There are two ways to do this that are by design from the Kotlin team:
As #Ronald pointed out in another comment:
Option 1: takeWhile { //predicate }
Cancel collection when the predicate is false. Final value will not be collected.
flow.takeWhile { value ->
value != "finalString"
}.collect { value ->
//Do stuff, but "finalString" will never hit this
}
Option 2: transformWhile { //predicate }
When predicate is false, collect that value, then cancel
flow.transformWhile { value ->
emit(value)
value != "finalString"
}.collect { value ->
//Do stuff, but "finalString" will be the last value
}
https://github.com/Kotlin/kotlinx.coroutines/issues/2065

Observe many times from same Observable (RxAndroidBle)

I'm using the RxAndroidBle library with RxJava2 to read from a BLE Characteristic. I think this question is just an RxJava question, but including the detail that I'm using RxAndroidBle in case that is useful.
I get connection, and then use it to call readCharacteristic(), which itself returns a Single<ByteArray>. At this point, I don't just want to just get the one ByteArray though. I need to read from this characteristic several times, because the BLE device is set up to let me get a small file back, and characteristics can only send 20 bytes back at a time, hence my need to read repeatedly.
Is it possible to modify this code so that the switchMap() below returns an Observable that will emit many ByteArrays, instead of just the single one?
I'm new to RxJava.
val connection: Observable<RxBleConnection> = selectedDevice.record.bleDevice.establishConnection(false, Timeout(30, TimeUnit.SECONDS))
return connection
.subscribeOn(Schedulers.io())
.switchMap {
// I want to get an Observable that can read multiple times here.
it.readCharacteristic(serverCertCharacteristicUUID).toObservable()
}
.doOnNext {
Timber.e("Got Certificate bytes")
}
.map {
String(it as ByteArray)
}
.doOnNext {
Timber.e("Got certificate: $it")
}
.singleOrError()
To repeat a read multiple times until a specific value is emitted one needs to change this part:
// I want to get an Observable that can read multiple times here.
it.readCharacteristic(serverCertCharacteristicUUID).toObservable()
to something like what was suggested by the RxJava author in the first answer that google gives for phrase rxjava single repeat:
// this will repeat until a `checkRepeatIf` returns false
Observable.defer {
val successValue = AtomicReference<ByteArray>()
connection.readCharacteristic(serverCertCharacteristicUUID)
.doOnSuccess { successValue.lazySet(it) }
.repeatWhen { completes -> completes.takeWhile { checkRepeatIf(successValue.get()) } }
}
I was able to get this working by sending a signal to stop both the connectionObservable, and the read on the Bluetooth characteristic. Of note is that you need to call toObservable() AFTER repeat() or this doesn't work, although I don't know why exactly.
override fun readMultipartCharacteristic(macAddress: String): Single<String> {
val CERTIFICATE_TERMINATOR = 0x30.toByte()
val device = bluetoothService.getBleDevice(macAddress)
if (connectionObservable == null || !device.connectionState.equals(RxBleConnection.RxBleConnectionState.CONNECTED)) {
connectionObservable = device.establishConnection(false, Timeout(30, TimeUnit.SECONDS))
}
val stop: PublishSubject<Unit> = PublishSubject.create()
return connectionObservable!!
.subscribeOn(Schedulers.io())
.takeUntil(stop)
.switchMap {
it.readCharacteristic(UUID("my-uuid"))
.repeat()
.toObservable()
.takeUntil(stop)
}
.collectInto(ByteArrayOutputStream(), { buffer, byteArray ->
// Watch for the signal of the end of the stream
if (byteArray.size == 1 && byteArray.get(0).equals(CERTIFICATE_TERMINATOR)) {
stop.onComplete()
} else {
buffer.write(byteArray)
}
})
.map {
String(it.toByteArray())
}
}
You can use the notification to buffer your data.
device.establishConnection(false)
.flatMap(rxBleConnection -> rxBleConnection.setupNotification(characteristicUuid))
.flatMap(notificationObservable -> notificationObservable) // <-- Notification has been set up, now observe value changes.
.subscribe(
bytes -> {
// Given characteristic has been changes, here is the value.
},
throwable -> {
// Handle an error here.
}
);

Persisting a network query and then checking whether another is needed with RxJava

This seems like it should be simple, but I'm struggling with the correct way of flowing the RxJava chain.
For instance, what isn't working is using flatMap to both persist data and check if another api query is needed:
return remote(amount = 2))
.subscribeOn(Schedulers.io())
.flatMap {
insertAll(it)
// Return an Observable<Boolean>, true if another api query is needed
shouldGetMore(it)
}
.flatMap {
if (it) remote(amount = 3)
// If another query is not needed just return an empty observable
else Observable.just(listOf())
}
.flatMapCompletable { insertAll(it) /* If another query was needed insert the result here */ }
.observeOn(AndroidSchedulers.mainThread())
With the above, insertAll() isn't called the first time because the function insetrtAll is a completable and is therefore not subscribed to in the flatMap. Instead only shouldGetMore() is executed. Now, here is my question:
For this to work I would need a flatMapCompletable at the beginning like so:
return remote(amount = 2))
.subscribeOn(Schedulers.io())
.flatMapCompletable { insertAll(it) }
// ...
But if I do this then I no longer have access to the results of the first api query to check if I shouldGetMore. My only thought is making insertAll not a Completable but rather an Observable that returns what was persisted after the fact, thus allowing me to complete the chain.
But this seems like an ugly approach, so I would be curious if anyone else had any other ideas?
Thanks.
The Completable offers the andThen operator to continue with any of the reactive base types:
return remote(amount = 2))
.subscribeOn(Schedulers.io())
.flatMap {
insertAll(it)
.andThen(shouldGetMore(it)) // <----------------------------------------
}
.flatMap {
if (it) remote(amount = 3)
else Observable.just(listOf())
}
.flatMapCompletable { insertAll(it) }
.observeOn(AndroidSchedulers.mainThread())