RxSwift: share()-ing is Caring

Welcome to Gett’s new Engineering blog! This is where individuals from our R&D will share stories, code samples, tips, thoughts, and experiments from their day-to-day work. We hope you’ll join us periodically and enjoy a quick break in your daily routine to learn some new techniques and enrich yourselves.


When you work with RxSwift, and Rx in general, every now and then you’ll discover some new piece of information that wasn’t entirely obvious to you until you’ve “hit a wall” and had to dig deeper. One of these common pitfalls is the concept of Resource Sharing. In RxSwift this is commonly expressed by the share() operator.

Before digging into the specifics of how and when share() is useful, or even required, let’s try and illustrate the problem at hand using a very common example:

A simple network request mapped to a name and phone when user taps a button

You have a result stream that reacts to a user’s tap on a button, and fires a network request to some API service. You map this result into two separate streams of the friend’s name and phone number.

Later on, in your View Controller, you’ll subscribe to the name and phone streams, and expect to get the latest friend’s name and phone when your user taps a button:

Bind your View Model’s output to your UI layer
Overview of a user’s tap processed by a ViewModel into a name & phone, via an API Service

This might seem like an innocent piece of code, but its not as simple as it might appear in the first place.

In the following example — every time you subscribe to phone or name — you’ll actually be getting an entirely different stream, meaning an entirely different network request per-subscription!

Each subscriber gets their own resource — and invokes the initial stream individually on each emission

This gets exponentially worse if you start branching off and mapping your outputs further:

Further branch the original stream by creating additional mappings

Each mapping is basically another subscription, thus creating additional independent resources and streams — causing additional network requests to be fired.

More mappings? More resources! Still individual and separated

In this case — there will be 4 network requests fired for every tap of the button. Yikes!

share() to the rescue!

Fortunately, an extremely useful operator called share() exists to solve this specific issue!

It lets you define streams that share resources among their subscribers. Meaning, each subscriber to the stream will get the exact same stream, and will not invoke additional computations or resources for that origin stream.

In the example above, if you merely change the original flatMapLatest example to the following, your entire problem will be solved and only a single network request per-tap is fired:

share() shares the stream’s resources among its subscribers

With share() added to your stream, here’s how the graph above looks now:

The stream is shared among all of its subscribers

Here’s another simple example to demonstrate this. The following code has a method, getUniqueId(), which returns an Observable<String> with a unique identifier whenever it is subscribed to.

This first example doesn’t have the share() operator. Notice how, even though all 3 subscribers allegedly subscribe to the same id — they get different streams, and you’ll actually see a different ID for each subscriber:

getUniqueId without resource sharing

Simply adding the share() operator at the end of your id stream makes things more expectable in this case, and you’ll see the same ID across all subscribers of that stream:

getUniqueId with resource sharing

Awesome! Now that you have a basic sense of what the share() operator does, it’s time to dive deeper into its additional capabilities and hidden corners.

Going deeper

You used the share operator in its most basic form, e.g. — share(), But there’s much more to know when you look into the full signature of this operator.

share(replay:scope:) — the full signature

When you call share(), you’re actually calling share(replay: 0, scope: .whileConnected) unknowingly since these are the default argument values of this operator.

Let’s break these two arguments down, on your way to becoming a master of using share()!

Replay

The replay count argument is quite self-explanatory. It basically means “How many elements would you like me to replay to new subscribers?”.

The default value of 0 makes your stream act much like a PublishSubject — e.g., subscribers only get future values emitted by the stream, but don’t know of anything that happened before the subscription point.

In contrast, setting a replay value larger than 0 is similar to a BehaviorSubject (for replay of 1), or ReplaySubject (for replay over 1) — e.g., each new subscriber would be replayed the last X events prior to the subscription point, along with any future emissions.

share(replay:) with various replay values

Scope

The scope argument has two possible values: .whileConnected (the default), and .forever.

Before explaining these two — I’d like to provide a full disclosure that whileConnected is considered the recommended scope, and for 99.9% of use cases, you probably wouldn’t want to change that. .forever could introduce some unexpected behaviour in your stream, but it’s still good to know of.

Here’s how each of these works:

  • .whileConnected: Values are replayed (when replay is larger than 0) in a reference-counting manner, much like ARC. When the number of subscribers drops from 1 to 0, the internal “cache” of the shared stream is cleared. It’s safe to use operators like retry since these will get a fresh stream for each retry and have a cleared internal state.
  • .forever: The internal cache of the stream is not cleared, even after the number of subscribers drops from 1 to 0. Meaning, future subscribers could potentially get stale events from the internal cache of the shared stream. It’s not recommended using operators such as retry in this case, as the retry might “carry” stale events and cause unexpected behavior.

Follow the notes in the following pair of screenshots to see the difference in action:

On the left side — a stream shared with a .whileConnected scope. On the right side — a stream shared with the .forever scope.

Shared Sequences are your (and your app’s) friends

RxCocoa, the Cocoa-specific companion library that’s part of the RxSwift project, contains a concept called a Shared Sequence. These are simply helper traits that already have some specific sharing behavior attached to them, by conforming to the SharingStrategyProtocol protocol.

The two shared sequence types in RxCocoa are Driver and Signal.

Driver and Signal — RxCocoa’a Shared Sequences

Both of these traits provide a shared stream with the whileConnected scope, with the only difference between them being thatDriver has a replay of 1, more fitting for representing state, while Signal has a replay of 0, which is more fitting for representing events.

It’s highly recommended using these traits to represent outputs related to your UI as they have additional guarantees such as always delivering events on the MainScheduler and never emitting an error event; quite useful for your app’s UI layer!

There’s more to learn about Drivers and Signals, but that’s out of scope for this post. If you want to learn more, check out the official RxSwift documentation on Traits!


That’s it for today!

I hope you’ve enjoyed getting a deeper understanding of how the share() operator works, and how RxCocoa’s Driver and Signal shared sequences rely on it.

Are there any concepts or operators of RxSwift you’d want to learn more about? Tell us in the comments, or reach out to me directly @freak4pc, and we might write about them! 😉