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
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:
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
phone streams, and expect to get the latest friend’s name and phone when your user taps a button:
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
name — you’ll actually be getting an entirely different stream, meaning an entirely different network request per-subscription!
This gets exponentially worse if you start branching off and mapping your outputs further:
Each mapping is basically another subscription, thus creating additional independent resources and streams — causing additional network requests to be fired.
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() added to your stream, here’s how the graph above looks now:
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:
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:
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.
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.
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
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.
scope argument has two possible values:
.whileConnected (the default), and
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
replayis 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
retrysince these will get a fresh stream for each
retryand 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
retryin 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:
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
The two shared sequence types in RxCocoa are
Both of these traits provide a shared stream with the
whileConnected scope, with the only difference between them being that
Driver 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! 😉