One of the most common practical problems in mobile apps is loading displayable data from the server, where the data can be anything from user’s feed or a list of podcasts to a profile picture or a streaming video.
Apps show various spinners and bars to indicate the loading process, all for inducing user’s patience and improving their experience.
However, this seemingly trivial problem has hidden pitfalls for a programmer, where a naive implementation can lead to writing excessive code and fixing sudden bugs.
An immutable struct for holding basic info about a podcast record:
The networking layer is represented with this tiny protocol; I’ll omit the factual networking code for simplicity of the example:
For the list of podcasts, we’ll need to have an observable array of
Podcaststructures. For that, we wrap
[Podcast] in the
BehaviorRelay class from
RxSwift, which is basically an observable property that always holds a value:
As you can see, we provided the ViewModel with access to the networking layer through a reference to
The array of
Podcast records is initially empty, but
loadPodcasts() function allows the user of the ViewModel to query the podcasts at the right time, and as the request completes it updates the list of podcasts.
A simple TableViewCell for displaying the Podcast info:
And finally, the ViewController that shows the list of podcasts:
Considering that the
viewModel is instantiated and injected to the
ViewController from the outside, now we have a fully functioning
MVVMmodule that automatically loads and displays the list of podcasts.
The only problem is that the app is currently not user-friendly. It doesn’t have an indicator for the loading process; neither does it show an error that can possibly occur in the networking layer.
Let’s update the ViewModel to address the challenge:
isLoading boolean property for tracking the loading status, and a
onError PublishRelay for reporting an error.
Now we need to update the UI and bind it with the status updates:
OK, now we’re showing
viewModel.isLoading reports that the podcasts are loading, and there is a
UILabel for showing errors emitted by
Note, that in the lines 16–17 we also had to explicitly hide the
podcasts has any records, because otherwise, the error message would stay on the screen even if the podcasts request succeeds after it initially errored out.
You can already sense a code smell. For such a simple use case we’ve already started fighting the state inconsistency.
Let’s take a deep breath in and out, and think for a minute. What are the possible states that our screen actually can be?
- Podcasts have not yet been queried
- Podcasts are being loaded
- Podcasts have failed to load
- Podcasts were loaded successfully
That’s right, just four cases! The superposition of the state values we have right now (
podcasts being empty or not,
onError) gives us
2*2*2 = 8 possible cases. This state redundancy is the reason why we had to implement a fix for conflicting values of
onError. In fact, this means there are other combinations we didn't consider yet, which can lead to unwanted effects, such as a simultaneous display of the error message and the loading indicator.
As we’ve identified the problem, we can refactor the current implementation to purge the state redundancy by introducing
enum with those four cases:
enum allows us to rework the
ViewModel in the following way:
With just one variable we were able to represent all the states we had, but now — without the state redundancy.
Refactoring the ViewController:
For convenient mapping the
Loadable<[Podcast]> in the code above we can use an extension for the
Great! Now our state — UI binding is much more clear and doesn’t require an excessive code for fixing visual defects.
If we want to display the previously loaded list while performing the list refresh, we can extend the
isLoading case with a parameter for holding the value.
The final implementation of the
Loadable, as well as a few more perks for it, can be found in the gist on Github; however, this was just an example of how to improve the clarity and stability of the reactive code by eliminating the state redundancy in the ViewModel.