Welcome to the series of articles on how to improve the quality of our iOS apps and become a more productive developer! This is the first article in series which full focus on the problem of shared state and how to possibly reduce it.
Mobile applications are becoming more and more popular every day. There are also many tools becoming available to develop apps, we have Swift for iOS and Kotlin for Android which promise us more compile time checks so that there’ll be less issues runtime.
However, despite sophisticated tools, it’s still tricky to write a stable application. In this article, I’ll share some tips from my personal experience on how to improve the quality of our software and make our lives a little easier.
Shared state and side effects
When developing modern mobile apps, we tend to use object oriented programming. We create objects, these objects can have fields which can be altered from many parts of our program. This can cause issues due to some part of the program expecting an object in a specific state and receiving something unexpected, leading to bugs. These bugs can be hard to reproduce on our local environment, since a specific set of steps need to be taken in order to reproduce the issue. Modern programming languages like Swift or Kotlin favor immutable data structures to reduce the issue. If we have an object whose fields are immutable, then once it’s created the object is in a stable state.
Let’s take a look at some practical example. Assume that we’re asked to implement a class which fetches user’s location in the world. Since this article is not a tutorial about how to use CLLocationManager, I’m going to focus on the interface only. One of the simplest ways to tackle the problem, is to use the delegate pattern, so it’d look something like this:
This solution has a few problems:
- The field delegate can be freely modified by other components. One place can set a delegate and another one can choose to assign it to another object, which can cause one of the components not receiving results whatsoever.
- We don’t know on which thread the results can will delivered on, without looking into the implementation. If we received this class as part of closed-source library, we’d either need to disassemble the binary or check it in runtime.
- What if we wanted multiple objects receiving the location updates? We’d need to change the class implementation, which would lead to public interface change and all relying components would need to updated.
In order to understand how to use observable streams in our applications, we need to change the way we think about designing applications. The most straightforward example of how it works is to think of an Excel spreadsheet. Let’s image that we have two cells in the spreadsheet and we want to sum them. That’s easy! Let’s take a third cell, take the addresses of two previous ones and write a formula that adds their values. Now, the cell will display the sum or an error if either of previous ones contained an invalid value (like a string instead of a number). Observable streams work in a similar way from a very high level point of view.
Now that we’ve got a basic understanding of how observable streams work, let’s look how the class interface would look like if we chose streams over delegate. For the purpose of this article, we’re using RxSwift library.
As we can see, problem #1 is gone. We have one immutable property location which emits location updates and no component can modify it. Our program not only more deterministic but since we don’t need a specific reference to the delegate, we got rid of additional protocol as well! Let’s see how subscribing to updated would look like:
This solves problems #2 and #3. As we can see, RxSwift allows us to specify on which thread we’re receiving the updates, as well as multiple objects can subscribe to a single stream to get updates via simple DSL-like API.
The problem with many ReactiveX articles out there, is that I didn’t see many of them outlining the disadvantages of this approach. All tools and libraries have good sides and bad sides and it’s up to us, developers to make the call to use them or not.
As I mentioned earlier, the main downside of using the observable streams approach is that we have to rethink the way we write mobile apps. This means that we’ll make many mistakes along the way and because of this, I highly discourage using ReactiveX in an important project for the first time. The best way is to find either a personal project or one that allows us to experiment and see if we like it. The learning curve for RxSwift is very steep and there aren’t that many good tutorials out there, so keep that in mind before using it for the first time.
Another disadvantage I experienced, is that while some things are very easy to implement using observable streams and make our code look nice and elegant, it doesn’t apply to everything. For example: there is a way to implement a UITableView data source using RxCocoa bindings, however it’s not straightforward and take longer rather than using the Apple recommended approach. My advice here, is to use observable streams selectively. Start with networking for example, make it work well and move on to other components and see if you really gain something by implementing them. Don’t blindly rewrite your whole app using RxSwift just because someone on the internet told you to do so.
In this article we introduced the concept of shared state and side effects and brought to attention on what issues can they create as well as my proposition on how to easily fix them using ReactiveX libraries. In the next articles, we’ll focus on other common issues in mobile apps and how to fix them.
Michał Tuszyński is a Developer at inFullMobile, an international digital product design and development studio based in Warsaw, Poland.
Fell free to contact us: firstname.lastname@example.org
A few useful tips to improve stability of your mobile app [vol. 1] was originally published in inFullMobile Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.