Development

Zipper RX Magic

March 10, 2020

As a developer you want to be able to fire off requests without worrying about what order they’re in or when they arrive. All you really care about is when they’re completed. And, with a little Rx magic, that’s when you stumble upon Single.zip:

`java /** 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…

`kotlin Single.zip( listOf(source1, source2, source3, ...) ) { results -> / do something / } .subscribeBy(

onError = Timber::e,
onSuccess = {
  celebrate()
}

) `

Everything appears to be going smoothly. Until you push the app out into the world and begin receiving crash reports.

The exception couldn’t be delivered to the user because it already canceled/disposed the flow. That, or the exception has nowhere to go in the first place.

Why is this? Like any responsible programmer, you added an onError condition to your subscription. But, instead of seeing it being called, you’re left with a crash that happens after the single has been completed.

You question how this could be happening and begin searching for the answer. Fortunately (or unfortunately), you begin to realize it’s entirely by design..

RxJava 2 tries to avoid losing exceptions which could be important to the developer even if it happens after the natural lifecycle of a flow.

Then you see one of the authors propose a couple of "solutions."

Override the default handler with RxJavaPlugins.setOnError() and suppress what you don't consider fatal. Alternatively, apply a per source onErrorReturn or onErrorResumeNext before zipping them together.

Though it would be nice to have a delayError flag similar to Observable.zip, you’re out of luck. Hey, we all occasionally forget to add an onErrorReturn to every one of our Single variables (although I strongly recommend taking this step).

Moving forward, I’ve been able to protect myself by using safeZip which automatically wraps all of your Singles, then returns all various errors along the way in a single error at the end.

`kotlin sealed class SafeResult { class Success(val result: T): SafeResult() class Failure(val error: Throwable): SafeResult() }

/** 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 zipSafe(sources: List<Single>): Single<List> { val safeSources = sources.map { source ->

source
  .map<SafeResult<T>> { SafeResult.Success(it) }
  .onErrorReturn { SafeResult.Failure(it) }

}

return Single.zip(safeSources) { it.filterIsInstance<SafeResult>() }

.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 problem. If an empty list is passed into Single.zip you will throw a java.util.NoSuchElementException exception. Though this will be handled by an onError in the subscription, if this is part of a larger stream, then the stream will have been completed. To avoid this issue you can make our safe zipper that much safer by returning an empty list when one is provided.

`kotlin /** * 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 zipSafe(sources: List<Single>): Single<List> { 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>() }

.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 }
    )
  }
}

} `

Success!

Scott Schmitz
Scott Schmitz
Software Developer

Stay in the loop with our latest content!

Select the topics you’re interested to receive our new relevant content in your inbox. Don’t worry, we won’t spam you.

MichiganLabs among INC. MAGAZINE’S BEST WORKPLACES 2019
Business Team Press Release

MichiganLabs among INC. MAGAZINE’S BEST WORKPLACES 2019

May 16, 2019

Michigan Software Labs is one of the highest-scoring businesses, with vibrant culture and deep employee engagement

Read more
Looking for a Front-End Framework? Our View On Vue
Web

Looking for a Front-End Framework? Our View On Vue

March 15, 2021

The programming world has always had its share of opinions. Our community loves debating everything from tabs vs. spaces to Vim vs. non-Vim to which frontend framework you should use. For me, the answer almost always comes down to: “Whichever, so long as it’s consistent across your project.” End of article. Thanks for reading!

Read more
Why Your Business Needs to Be In the App Store—Yesterday
Business

Why Your Business Needs to Be In the App Store—Yesterday

January 20, 2021

Whether you’re a Fortune 500, a startup, or somewhere in the middle, there are countless reasons to consider developing a mobile smartphone app. From giving customers a more complete brand experience to gaining credibility in the marketplace, here are just a few of the benefits of having an app.

Read more
View more articles