Snail: A Lightweight Observables Framework

Available for iOS (Swift) and Android (Kotlin)

Russell Stephens
Compass True North
5 min readMay 18, 2017

--

Github

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.

Typical Unwieldy Async Callback

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.

Events Occurring Over Time

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.

Subscribing to an Observable

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:

  1. You don’t specify the queue. Your observer will be notified on the same thread as the observable published on.
  2. You specified main queue AND the observable published on the main queue. Your observer will be notified synchronously on the main queue.
  3. 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:

  1. Strong — the default, increase reference count by 1 in each direction i.e. self.thing
  2. Weak — does not increase the reference count, calls to self are now optional i.e. self?.thing
  3. Unowned — similar to weak, but non-optional i.e. self!.thing
We always recommend using [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 or init for views and view models
  • To ensure UI code executes properly, make sure to subscribe on the main queue

Github

--

--