As a develÂopÂer you want to be able to fire off requests withÂout worÂryÂing about what order they’re in or when they arrive. All you realÂly care about is when they’re comÂpletÂed. And, with a litÂtle Rx magÂic, that’s when you stumÂble upon Single.zip
:
/**
* Waits until all SingleSource sources provided by the Iterable sequence signal a success
* value and calls a zipper function with an array of these values to return a result
* to be emitted to downstream.
...
Next, you pass along your list of Single
into Single.zip
like so…
Single.zip(
listOf(source1, source2, source3, ...)
) { results -> /* do something */ }
.subscribeBy(
onError = Timber::e,
onSuccess = {
celebrate()
}
)
EveryÂthing appears to be going smoothÂly. Until you push the app out into the world and begin receivÂing crash reports.
The excepÂtion couldn’t be delivÂered to the user because it already canceled/​disposed the flow. That, or the excepÂtion has nowhere to go in the first place.
Why is this? Like any responÂsiÂble proÂgramÂmer, you added an onError
conÂdiÂtion to your subÂscripÂtion. But, instead of seeÂing it being called, you’re left with a crash that hapÂpens after the sinÂgle has been completed.
You quesÂtion how this could be hapÂpenÂing and begin searchÂing for the answer. ForÂtuÂnateÂly (or unforÂtuÂnateÂly), you begin to realÂize it’s entireÂly by design..
RxJaÂva 2 tries to avoid losÂing excepÂtions which could be imporÂtant to the develÂopÂer even if it hapÂpens after the natÂurÂal lifeÂcyÂcle of a flow.
Then you see one of the authors proÂpose a couÂple of ​“soluÂtions.”
OverÂride the default hanÂdler with
RxJavaPlugins.setOnError()
and supÂpress what you don’t conÂsidÂer fatal. AlterÂnaÂtiveÂly, apply a per sourceonErrorReturn
oronErrorResumeNext
before zipÂping them together.
Though it would be nice to have a delayError
flag simÂiÂlar to Observable.zip
, you’re out of luck. Hey, we all occaÂsionÂalÂly forÂget to add an onErrorReturn
to every one of our Single
variÂables (although I strongÂly recÂomÂmend takÂing this step).
MovÂing forÂward, I’ve been able to proÂtect myself by using safeZip
which autoÂmatÂiÂcalÂly wraps all of your Single
s, then returns all varÂiÂous errors along the way in a sinÂgle error at the end.
sealed class SafeResult<out T> {
class Success<T>(val result: T): SafeResult<T>()
class Failure<T>(val error: Throwable): SafeResult<Nothing>()
}
/**
* Zip [Single] together safely. An onErrorReturn is automatically applied to each source
* to prevent any source from throwing. Then after all sources have completed, any errors
* are then reported
*/
fun <T> zipSafe(sources: List<Single<T>>): Single<List<T>> {
val safeSources = sources.map { source ->
source
.map<SafeResult<T>> { SafeResult.Success(it) }
.onErrorReturn { SafeResult.Failure(it) }
}
return Single.zip(safeSources) { it.filterIsInstance<SafeResult<T>>() }
.flatMap<List<T>> { safeResults ->
val failures = safeResults.filterIsInstance<SafeResult.Failure<T>>()
if (failures.isNotEmpty()) {
Single.error(CompositeException(failures.map { it.error }))
} else {
Single.just(
safeResults.map { (it as SafeResult.Success<T>).result }
)
}
}
}
Except there’s still a probÂlem. If an empÂty list is passed into Single.zip
you will throw a java.util.NoSuchElementException
excepÂtion. Though this will be hanÂdled by an onError
in the subÂscripÂtion, if this is part of a largÂer stream, then the stream will have been comÂpletÂed. To avoid this issue you can make our safe zipÂper that much safer by returnÂing an empÂty list when one is provided.
/**
* Zip [Single] together safely. An onErrorReturn is automatically applied to each source
to prevent any source from throwing. Then, after all sources have completed, any errors will then be reported.
*/
fun <T> zipSafe(sources: List<Single<T>>): Single<List<T>> {
if (sources.isEmpty()) {
return Single.just(emptyList())
}
val safeSources = sources.map { source ->
source
.map<SafeResult<T>> { SafeResult.Success(it) }
.onErrorReturn { SafeResult.Failure(it) }
}
return Single.zip(safeSources) { it.filterIsInstance<SafeResult<T>>() }
.flatMap<List<T>> { safeResults ->
val failures = safeResults.filterIsInstance<SafeResult.Failure<T>>()
if (failures.isNotEmpty()) {
Single.error(CompositeException(failures.map { it.error }))
} else {
Single.just(
safeResults.map { (it as SafeResult.Success<T>).result }
)
}
}
}
SucÂcess!
Looking for more like this?
Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.
![Automatic artifact downloads inside PR comments](https://optimise2.assets-servd.host/gratis-creeper/staging/blog/automatic-artifacts-thumbnail.jpg?w=1080&h=1080&auto=compress%2Cformat&fit=crop&dm=1718905906&s=8d3b105c2c2ee23c5ae17003fdb876d9)
Automatic artifact downloads inside PR comments
June 20, 2024Discover a method to streamline the process of accessing build artifacts from GitHub Actions by reducing the number of clicks needed to download them directly from a pull request (PR) comment.
Read more![Advanced Tailwind: Container Queries](https://optimise2.assets-servd.host/gratis-creeper/staging/blog/banner_2023-07-28-190954_vhsa.jpg?w=696&h=320&q=75&auto=format&fit=crop&dm=1708017435&s=f3161781a39c23295bbfcdd4b8e65868)
Advanced Tailwind: Container Queries
July 28, 2023Explore some advanced web layout techniques using Tailwind CSS framework
Read more![Web app vs. mobile app: How to decide which is best for your business](https://optimise2.assets-servd.host/gratis-creeper/staging/blog/web-app-mobile-app-thumbnail.jpg?w=1080&h=1080&auto=compress%2Cformat&fit=crop&dm=1711474200&s=f0a11a5e2c6c8c51536fa06ae1729418)
Web app vs. mobile app: How to decide which is best for your business
March 26, 2024When considering whether to develop a web app or a mobile app for your business, there’s honestly no definitive answer. But this article will help you make an informed decision that aligns with your business goals and sets you up for success.
Read more