Snail: A Lightweight Observables Framework
Available for iOS (Swift) and Android (Kotlin)
Github
- iOS (Swift) — https://github.com/UrbanCompass/snail
- Android (Kotlin) — https://github.com/UrbanCompass/Snail-Kotlin
We live in a synchronous world. Although we believe we can multitask, for the most part we accomplish our tasks one at a time. For example, when we order a new item from Amazon, we know that it will appear in the mail in two days. We do not need to figure out where the closest warehouse with that item is, nor do we need to coordinate with UPS to pick up and drop off these packages.
Software is not so lucky.
When building a software system, we must account for these nuances, which in turn, can add a layer of complexity. We all know that asynchronous code can be ugly, see the image to the left; this is often handled via layers of callbacks which often turns into spaghetti code.
How a system hides this complexity separates a good software system from a great one.
Synchronous code is easier to read, write, and understand.
Therefore, it’s reasonable to believe that writing asynchronous code in a more synchronous way can help relieve these inherent complexities.
Enter Snail
A Swift & Kotlin implementation of the observable pattern, Snail provides asynchronous solutions in a synchronous way. This article serves to highlight the four main benefits of Snail:
- functional
- thread-safe
- less is more (compared to RxSwift)
- easy error handling
Although Snail is available in both Swift and Kotlin, the following code examples will be provided in Swift. For examples of Snail in Kotlin, please refer to the examples here.
Observables
I like to think of observables as a timeline. Just like in real life, there will be events like a graduation, or a birthday at certain points along the way. However, the significance of these events is amplified by interested parties such as family and loved ones that view and process these events. In the realm of observables, these interested parties are referred to as subscribers.
In code, observables can be thought of as a stream of events. As different events transpire during your application’s lifecycle, any events sent through an observable will be forwarded to its subscribers.
At the core of Snail are Observables, which as the name implies, can be observed or subscribed to for one of the following three events:
- next: the next event
- error: a reason why it failed
- done: a callback for cleaning things up
The observable class relies on Generics, so out of the box, it just works.
When the underlying value of the observable is changed, any subscribers to the observable are notified.
Specific situations may require other functionality, so Snail comes with four types of observables:
Observable
The Big Mac™ of observables. It forwards events to its subscribers.
Just(value)
Forwards the given value to the next closure, then triggers done.
Fail(error)
Forwards the given error to the error closure, then triggers done.
Replay(n)
When a new subscriber is added to the observable, the previous n events are forwarded to the next closure. If a previous event was an error, only the error will be forwarded to the error closure.
Replay is extremely useful in the case, where events might occur prior to hooking up a subscriber i.e. returning an observable that might have an initial value from cache.
Sending Events through the observable stream is easy.
Variables
Using observables directly can feel a little unusual. The Variable class offers the power of the observable, with the readability and usability of a var
or let
Turning a Variable<T>
into an Observable<T>
can be done via the function asObservable()
then you can subscribe to it like any other variable.
Queueing
We would be remiss if we did not mention queueing, also known as threading. Given our previous examples of events for observables, queueing is what gives software the advantage over real life. Imagine, for example, you have two events taking place at the same time — like two separate graduations at two separate schools. Although you may try your best, it would be hard to give each event your full attention.
Queues allow what we would call multitasking to occur almost seamlessly by allowing subscribers in different queues to share execution time. This would be similar to splitting each graduation into separate events and attending both, but within the same time and space.
You can specify which DispatchQueue a subscribe block will be executed on by using .subscribe(queue: <desired queue>)
. If not specified, then the observable will be notified on the same queue that the observable published on.
There are 3 scenarios:
- You don’t specify the queue. Your observer will be notified on the same thread as the observable published on.
- You specified
main
queue AND the observable published on themain
queue. Your observer will be notified synchronously on themain
queue. - You specified a queue. Your observer will be notified async on the specified queue.
Avoiding Strong Retain Cycles
This section is relevant to iOS specifically.
Although we get a lot for free with ARC, we still need to think about memory management for our subscribers.
Apple provides a guide on retain cycles here, and this post by Hector Matos is a great reference.
Swift has three types of ownership:
- Strong — the default, increase reference count by 1 in each direction i.e.
self.thing
- Weak — does not increase the reference count, calls to self are now optional i.e.
self?.thing
- Unowned — similar to weak, but non-optional i.e.
self!.thing
[weak self]
to prevent strong retain cycles.Alternatives
RxSwift
RxJava is a Java VM implementation of Reactive Extensions: a library for composing asynchronous and event-based programs by using observable sequences.
It extends the observer pattern to support sequences of data/events and adds operators that allow you to compose sequences together declaratively while abstracting away concerns about things like low-level threading, synchronization, thread-safety and concurrent data structures.
Our Thoughts
RxSwift and the approach required to use an observables framework is immense. Prior to Snail, our iOS apps were built using this framework. However, as our apps became more mature, we began to realize that we really only needed a subset of these features, mainly, observables and variables. We slowly began to consider the rest of the framework as bloated and feeling the effects of feature creep.
PromiseKit
Modern development is highly asynchronous: isn’t it about time we had tools that made programming asynchronously powerful, easy and delightful?
Our Thoughts
Promises are a great way of encapsulating asynchronous tasks and chaining various blocks of code together to avoid callback hell. Promises can be very powerful, but the paradigm is much different than an observable-based architecture.
In Practice
- We tend to keep our bindings together in one function called
setupBindings
- We register most bindings after
viewDidLoad
for controllers orinit
for views and view models - To ensure UI code executes properly, make sure to subscribe on the
main
queue
Github
- iOS (Swift) — https://github.com/UrbanCompass/snail
- Android (Kotlin) — https://github.com/UrbanCompass/Snail-Kotlin