Building flows on Mobile with LEGO bricks

Philip Engberg
Tonsser
Published in
9 min readMar 25, 2018

Note: This post mainly uses examples with Swift on iOS, but at the end of this post, examples using Kotlin on Android are also provided.

Flows in mobile apps are ubiquitous these days, yet supporting complex, non-linear flows that have extremely low maintenance and ridiculously high flexibility is not as straight forward as one could hope.

The Tonsser iOS app has many small and medium sized flows, most of which are more or less linear. However, our product requires our onboarding flow to be very non-linear. At the same time, being a lean and agile startup, we want to be able quickly swap around, swap out or A/B test indivisible steps, or even entire sub flows with a snap of the fingers. There are of course a multitude of ways to attack this problem, but the nature of Rx — handling asynchronous behaviour in linear fashion — is an obvious choice to begin with, and since our app already heavily relies on this paradigm, we endeavoured on a journey to find the ultimate flow management that would be both

  • Super easy to implement, maintain, and expand on, and
  • Support very high level of complexity in the underlying flow logic

The challenge

This is the latest iteration of the Tonsser onboarding flow. You might ask: “Well, why are you making it so hard for yourself?”. It’s not because we specifically want to, it’s because… football — or soccer, depending on your place of origin ⚽️🌍

Tonsser is an app for football players to keep track of their match performances, among other things. But since this is our main value proposition, we want to present this value to users as early in their journey as possible, so that they understand why Tonsser is useful for them.

But inputting data about a match requires that the team you played on actually did play a match in the past. But matches are only played during the season, in a relatively short window, and you may sign up at an arbitrary point in time relative to this. On top of that, in order for you to even be able to input match stats to begin with, you first need to find your real life team, and since we get all this team and match data from third parties, data may sometimes be missing or incomplete, meaning you can’t find your real team, so a lot of fallback flows are needed. Oh, and we recently added support for coaches, who have their entirely own flow and journey 🤯

So all in all, there are many paths through our onboarding and implementing that flow logic, let alone maintaining and extending it with low cost is a potential nightmare.

Rx to the rescue

As mentioned, we are huge fans of Rx and use it for almost everything. Rx helps handling asynchronous units of work in a very elegant way and in the case of flows there are two main kinds of asynchronicity:

  1. 👆 The behaviour and interaction of the user: After displaying a certain step in the flow, it may take an arbitrary amount of time before the user chooses to continue.
  2. 📡 Any network related work associated with performing a step in the flow: Like POSTing new data to the API once a flow step in finished.

If you were to use pure Rx for this, one approach could be to let each step expose and observable that internally is kept as a PublishSubject. Then the individual step could call onNext on this whenever it’s done and then have some flow controller subscribe on this and move on to the next step. But we can actually do better than that.

Actions

Action is an abstraction of observables that was carried over from ReactiveCocoa’s RACCommand. It’s a way of deferring a unit of work, which produces an observable, to be executed at any point later in time. This may simply represent a UI interaction or and API request.

An action is defined as Action<Input, Element> and is initialised with a workFactory defined as

So it’s essentially just an anonymous function that takes one parameter of type Input and returns an observable of type Element. To execute this work factory you simply just call execute() on the Action:

An action then exposes three different observables for you to subscribe on as needed:

  1. .elements: Observable<Element> This emits the observable returned by the work factory you defined on initialisation, so the outputs essentially.
  2. .errors: Observable<Error> This emits any errors that occur in the work factory in a separate observable as next elements, meaning that errors don’t cause the .elements observable to complete and thus lets you observe errors happening infinitely many times.
  3. .executing: Observable<Bool> This is a convenient property that is basically meant to bind to a spinner, so that you don’t have to manually show a spinner before you start executing and then stop it if it completes or if it errors.

Also, actions can only execute one thing at a time, so if the workFactory is already executing (e.g. if an API call is already underway) it will just discard any calls to execute() until it’s done.

For further convenience the Action comes with a handy extension to UIButton allowing you to bind these two directly together:

button.rx.action = action

Actions in flows

That’s all great, but how does that fit into flows? 🤔 Since steps in a flow typically end with the user tapping a button, which may make one or more API requests, it would make sense to represent this with actions in each step and then have a separate flow controller listening on these in order to know when to move on.

Consider this simple linear flow:

Each step exposes one doneAction and does not know anything about any of the other steps in the flow. Now these three steps can be composited together by flatMaping the doneActions as seen below. Also notice how only Step A performs an API request whereas B and C just represent UI interaction, but that the flow controller does not have to know or care about this.

This is already pretty simple and easy to understand — but so is the flow it’s representing.

Non-linear flows

Now consider the flow above. Here the user has two options on Step B: A primary option, represented by the doneAction, and a secondary action, represented by the skipAction. Depending on which of these the user selects, different steps will be shown next. This can be achieved by modifying the FlowController as follows:

So now Step B handles the forking itself and everything is still relatively simple. But we’re just getting started 😱

Shared subflows

Now, this is when it starts getting a little annoying to handle 🙄 At different, independent points after a fork in the flow, the user will get E, F, and G in that order always. Now we are forced to duplicate flow logic code with a bunch of flatMaps, yuck.

And this is only one layer deep in forking complexity — imagine just one or two layers more.

Higher order generic functions with infix operator 👀

So we want to be able to make a reusable definition of the subflow E, F, and G. So what we need is something that can composite an arbitrary number of steps together into a single variable, which can then be executed at any point in the flow. This composite function should take two arguments: The function that performs the first step (like showE()) and a function that performs the next step function (like showF()), both of which of course return observables. A flatMap can then be applied to the first function, in which the next one is invoked. If all of this is returned in a wrapping function, this process can be repeated over and over again 😃

Alright, enough theory. Below is a custom, generic infix operator defined, which takes in two functions as parameters, both of which return observables. Furthermore, the return type of the first function has to fit with the input of the second function in order to be able to chain them together.

Using this, we can now define the E, F, G subflow globally and execute at any point in the flow like this:

😍😍😍

Not only does this significantly improve clarity and readability, but if you all of a sudden have to swap around F and G it just takes a few seconds and you only need to do it once:

let showEGF = showE |> showG |> showF

💥

Concrete example

Below is a concrete example from our new onboarding flow. You may use the flow chart in the beginning of this post to get an idea of the purpose of this logic. Early on in the flow, the user has to pick his or her role in the app. Coaches and Players each have their own full fledged flows, whereas selecting a different role from these simply skips to the shared end flow, which can be seen in forkFlowBasedOnUserRole().

Similarly, if you did select the Player role, you are taken through the joinTeam flow — which, by the way, has its entirely own isolated flow controller setup like the onboarding flow for reuse in-app. This subflow may result in you not finding your team, in which case you are taken to a fallback subflow, which then again is composited together with the same shared end flow as above 💖

The power of LEGO bricks

So with the help of Rx, Actions, and the custom infix operator |>, we can piece together incredibly complex flows in a way that is completely abstracted away from the UI and functionality of each step, while being able to maintain, modify, or even A/B test any part of flow logic in little to no time — just like building with LEGO bricks instead of wooden sticks and hot glue 🤘

Android & Kotlin

Kotlin is in many ways very similar to Swift, which makes it easy to share ideas and concepts across iOS and Android.

That is also what happened in this case. Our Android app is also heavily relying on RxKotlin, so using the Action approach seemed like an obvious choice for this onboarding flow. But after sifting through the World Wide Web, we were left disappointed 😥 So we took matters into our own hands and ported the Swift version ourselves. We of course open sourced it as well and can be found here: github.com/Tonsser/KAction.

The definition of the infix function if very similar to that in Swift:

It’s an extension of the Function1 type, i.e. functions that take one argument. Kotlin can’t use special characters in function names because of Java interoperability, so we went with a simple then instead 🤓

In order to pass functions as parameters to other functions in Kotlin, you have to use :: operator. Putting all the pieces together, this is what we end up with:

Subscribe to the Tonsser Medium blog to get an in depth story in the near future of how our Android Engineers managed to implement this insanely flexible, yet powerful flow architecture as well 💪

At Tonsser we seek to change to the world of football using cutting edge technologies. We are constantly looking for daring explorers like you, so check out our open positions at tonsser.com/jobs.

--

--