Optimistic Updates with RxKotlin and ItemTouchHelper (Android)

One of the most frustrating and rewarding features I worked on during my co-op was reworking a settings page for users that featured drag-and-drop and optimistic updates. The problems that arose showed me the subtle complexities of such a feature. They also cautioned me to write simpler, readable (and ergo debuggable) code so design flaws are less often confused with technical bugs.

General use case: Manage groups of ordered items, where individual items can be moved between groups

User Story
As an Android Hootsuite user, I want to be able to control the content that is displayed to me. To that end, I should be able to group related social networks/streams together into tabs and rearrange the order of tabs as well as the streams within each tab so that the highest priority content streams are easily accessible and related content is grouped to my liking.

(content) streams :: Each social network (SN) provides different content sources. For example if you add a Twitter account, you might have a “My Posts” stream (featuring all your posts), or you can define your own by defining a search for a hashtag/query.

tab :: A grouping of content streams. Loading a tab will allow you to swipe across all the contained streams.


Problem Overview

(Left) Old tabs and streams management (Right) New tabs and streams management — what we’re building

Here’s a detailed breakdown of the features:

  • A list of heterogeneous items — Tabs (category headers), Streams (item), Empty Placeholder (prompts user to Add Stream if Tab is empty), Footers (user dependent)
  • Tabs that contain no Streams instead have a placeholder which performs the same function as “add stream” from the Tab overflow menu
  • Footers with their own actions that are appended to the very bottom of the list
  • Popup overflow menu on each Tab item to: add stream, rename, reorder, delete
  • Drag handle/long-press on Stream that allows dragging and dropping
  • Left swipe on Stream to remove it from the Tab

Before redesigning this functionality, our existing implementation featured some but not all of these features.

In addition to discoverability issues, a main source of gripe is the blocking ProgressDialog (deprecated) on each action. Therefore, it would be nice (but overwhelmingly frustrating for me) to add the following features:

  • Non-blocking re-arrange actions (optimistically update UI)
  • Snackbar notification with Undo action

Our implementation prioritizes:

  1. Maintainability and Extensibility — this is the most important technical lesson I learned during my co-op. Maintainability means writing code knowing that someone else will have to dig through it at some point to change/add functionality. As the cliche goes: there’s readable code and then there’s code that no one uses.
  2. Smoothness — we are relying heavily on touch interactions to effect state changes. We want to avoid instances such as forcing the user to drop a held item when we refresh/display a snackbar or needlessly refreshing the display and blocking user interaction.

Android UI components

From the problem overview, we can already gain a sense of the UI components we’ll need.

A list of heterogeneous items: A RecyclerView with multiple view types and view holders. We maintain an RecyclerView.Adapter<Any>; we forego adding an additional layer of abstraction e.g.RecyclerView<ManageItem> as there is no real added benefit. We will still leverage getItemViewType regardless. Our code base already abstracts the onBindViewHolder and onCreateViewHolder into a superclass with a SAM interface that handles interaction callbacks.

View interactions specific to view type: As mentioned above, our SAM interface will handle any effectful interactions.

Swipe-to-Delete and Drag-and-Drop: Use the ItemTouchHelper to animate and handle these interactions.

Architecture Pattern

First things first — what view architecture would make our lives easier? Lately, we’ve been moving more towards the Model-View-ViewModel (MVVM) architecture for Android features at Hootsuite. Here are several reasons why I believe this is the best pattern for this use case:

  • Well-supported by Android Framework, retention of state through orientation change.
  • Separation of business logic from the view, making complex transformations easily unit testable. When we make/undo a change, repopulating the data or reverting failure states can be individually tested. We have full control over the data model and don’t need to programmatically trigger drag/drops or swipes.
  • Abstracting common logic between similar use cases. Re-ordering Tabs has much of the same logic as re-ordering Streams (albeit much simpler).
  • Already high coupling required on View. Our custom RecyclerView already has interaction listeners to support Swipe-to-Refresh and Jump-to-Top. Each list item may have additional view interactions. We will also be adding ItemTouchHelper. Can we centralize this responsibility?
Simplified UML class diagram of implementation. Several classes omitted.

With optimistic updates, it would be nice to have a single-source of truth for UI state. MVC might do. But consider the fact that some changes to the model e.g. database update, do not reflect instantly on the view because we’re updating them optimistically and asynchronously. We want an intermediary for any change. In combination with Rx, ViewModel’s are up to the task! They will allow us to reactively coordinate changes from a multitude of view interactions.


Maintainability

I was delighted to find that our existing code base had an excellent abstraction for handling many different list item layouts in a single list (RecyclerView). The more interesting and challenging consideration was writing readable code; readability distinguishes easily maintainable and debuggable code from giant swamps of degrading code quality.

Here is an example where I made a tradeoff between the (negligible) space complexity of modifying a list in-place with the readability of more expressive code, taking advantage of Kotlin syntax.

The purpose of the functions are the same: given a list of items, we want to insert empty placeholders or remove them from the appropriate places. E.g. two Tabs in a row indicate one of them is empty and a placeholder should be inserted. We write it as an extension function since it is always used in the context of our list of data, thus we implicitly assign it the responsibility of re-arranging itself correctly.

Disgusting. It really is. But it totally works. What’s so bad about it?

  • It assumes familiar knowledge of the iterator cursor. When you insert an item, where does it go? What gets removed?
  • Peeking ahead is impossible with our cursor, we have to advance to check the condition and retreat to perform certain actions
  • Nested ifs inside a switch statement inside a loop.

Overall, the problem is this: the context of each line is hard to gleam from line to line. Where are we in the list? Where is the cursor? Where do we need the cursor to be in order to remove this item? In sum, in-place relies on side-effects. Side effects are sometimes hard to keep-track of and read.

Let’s try a slightly different, more functional fold approach; though not in-place, both implementations are still O(n).

Why do I consider this implementation superior?

For those unfamiliar with the fold method, here’s a quick review. Fold is a lot like a for-each loop, except that it returns a single value. Think of it as initializing an “accumulator” variable, and for each item in the collection, you will place into it a new value. At the very end, the value inside your accumulator is returned. Here are a few examples:

With our fold implementation, we are able to leverage the Kotlin’s syntax to improve readability. As Trevor Stokvis reminded me in conversation, it’s often useful to use method names as documentation. Here, the purpose of these functions are very clear. In fact, they even clarify the context at their call-site — something that we lost with the indexing of the iterator.

Finally, we can read the code in a simpler way: we are progressing through our list, slowly accumulating certain items. We don’t have to worry about backtracking and removing elements.


Smoothness

An important UX consideration is what to do when our optimism succeeds, i.e. the server accepts our change request.

One way to do optimistic updates is to allow the state change and further actions, and when the success response is received, we update the screen to the successful state.

Below is a demo on Android and iOS. In both, we are rearranging streams between and within a category. On the right, we see what happens when we re-draw the screen when our optimistic update query returns successfully. On the left is where we silently accept the success and don’t interrupt the user.

(Left) Android; (Right) iOS

In other words, the iOS implementation will replay successful actions, which interrupts the user and acts as a pseudo-blocking state.

What we want to do is only revert failure states. This means that we should be even more optimistic. This is where the big headache begins. We should be able to base our changes off uncommitted states, that we hope will soon be committed.

Action States

We convey the change using an PublishSubject that emits a sealed class

sealed class DataEvent(val data: List<Any>) {
class RefreshSuccess : DataEvent()
...
}
val subject: PublishSubject<DataEvent> = PublishSubject.create()

Then we define the different event states possible: RefreshSuccess, RefreshFailure, MovePending, MoveSuccess, MoveFailure, UndoPending, UndoSuccess, UndoFailure. This makes it easy to view each action flow as a series of finite-states, where the transition functions can be user input or api calls/responses. When testing the view model, we can mock/invoke these transitions between states and verify the result emitted by the view model subject.

The MoveSuccess is an interesting special case, since it’s meant to be “undoable”. We need to record the previous state as well as the desired state. Depending on your server endpoint, you have to consider how to reverse the state change. If all the logic is server-side, then you’re in luck! Sadly this wasn’t the case and the inverse move action had to be computed, sent, and handled.


Conclusion

This setup is quite flexible to extending functionality.

  • The ItemViews themselves are modularized, they can be re-skinned and have their actions changed
  • The ItemTouchHelper can be modified to support swipe actions
  • Business logic lives exclusively in the ViewModel, changes will be localized and testing the view model covers most use cases

There are some considerations to make though:

  • There is a lot of code due to all the different actions that can be performed on the screen. A screen as complex as this might be divided and simplified.
  • This ‘optimism’ flow can be entirely circumvented by using an “edit” mode with “done” button that commits the changes, depends on the use case.

Though I ran into many dead-ends and questions that led to more questions, slowly working through and iterating an implementation was incredibly rewarding. It wasn’t so much writing the code that tripped me up, the main consideration was modularizing each piece so that it could be changed or added to. An unexpected side effect was that it was much easier to diagnose and improve my solution. Because all the code for a particular functionality had a logical place, it was easy to form hypothesis and work it out.

That being said, all the credit goes to the amazing UX team and mobile team I had the pleasure of working with here at Hootsuite. The support, patience and cordiality shown to me kept me motivated and excited about implementing from a user-minded perspective.

About the author

Augustine Kwong is a Combined Major in Computer Science and Statistics at the University of British Columbia. He worked on the Core Mobile team, working predominantly on the Android app but branched out to work on backend in Scala.