Friday, May 10, 2019

UI Architecture Design Patterns

At Google I/O 19, the Android Jetpack team announced Compose, which basically brings a React style UI toolkit to Android. Awesome.

For those who haven't worked in Javascript land before, I highly recommend you take a look at React and read about its philosophy behind the handling of state and data.

tldr; By enforcing a strict 1-1 relationship between a given component's state and the actual visuals drawn, you can be sure that data is always synchronized -- what ever business logic manipulates is what the screen will draw.

However, as with all nice things, Compose is not ready yet for prime time. In fact, it's not even ready for developer time - as it currently is in a pre-alpha stage. Google does recommend though that you prepare your applications for a world where a one way composable view state becomes the norm - and we don't have to wait at all to start doing that!

-----------------------------

Recently I was inspired by a talk given by Netflix engineers at a Droidcon event back in 2018 where they outlined how Netflix approached UI building via independent components which they composed into a screen. I adapted their architecture into the following couple of basic classes.

UiView.kt

@CheckResult
@IdRes
fun id(): Int

fun inflate(savedInstanceState: Bundle?)

fun saveState(outState: Bundle)

fun teardown()



UiViewModel.kt

fun bind(onRender: (state: T, oldState: T?) -> Unit)

fun unbind()



UiComponent.kt

@CheckResult
@IdRes
fun id(): Int

fun bind(
  owner: LifecycleOwner,
  savedInstanceState: Bundle?
  callback: C
)

fun saveState(outState: Bundle)





The UiComponent class would take any number of related UiViews and UiViewModels and would result in self-contained components that could be dropped into Android Views, Activities, or Fragments. Not bad - but there were many shortcomings with my implementation.

The application state was a two way flow, which would prove to be difficult to manage. Because a UiComponent was meant to be self contained, it was both a View and a Controller. My attempt at separating logic out of Activities and Fragments did not actually fix anything - it just changed the name of the problem to UiComponent. As a result, the relationship between UiView and UiViewModel was only loosely defined - sometimes the view would listen to the model and change how it displayed content on screen, sometimes the model would listen to the view and fire off navigation events using some rather hacky logic.

There also was the issue of creating the UiComponent class for each of these UI widget groupings - it was an extra file that really did not do much on its own except abstract other classes needlessly. It had to be better.

I went back to the drawing board and reasoned about how I could simplify this basic 3 class architecture I had built.

My first win was getting rid of the UiComponent class all together. It was basically a useless extra - it served as a Controller for the UiViews passed to it - but I had already established that in my Android architecture the Activity or Fragment was the controller. There was no need to have a Controller wrap another Controller, so UiComponent was removed.

UiView was a little bit trickier. Because Android's current view system is a far cry from reactive composable state, I had to make do with the current UI toolkit - which means I was required to have an inflate(Bundle?) method as that was the step where Android would inflate layout resources and find views and whatnot. Similarly, I needed to keep a teardown() method to release resources, as well as a saveState(Bundle) method for process death persistence. The id() method helped with laying out my UI widgets inside of ConstraintLayout containers, so it too stayed. Looks like I wasn't able to simplify UiView any amount - but that doesn't mean I was not able to improve it.

The main problem with UiView is that there was no established contract for when a UiView updates. Taking a page from the React toolkit, I added a render(T, T?) method which I would adopt as the only place to make dynamic UI updates in a UiView class.

Next I tackled the UiViewModel. My main issue with this class was that it required an arbitrary lifecycle of bind() and unbind() in order to make it possible to use at all. After working at it for a while, I managed to reduce UiViewModel to just one function, render() which will treat its lifecycle as the lifecycle of the RxJava stream it associates state events with. Meaning, once the RxJava stream opens, the ViewModel is "bound" and once the stream is disposed, the ViewModel is "unbound".

Instead of manually passing a callback to and from ViewModels and UiViews, I used an RxJava bus - one for the UiView as a stream of interaction events, and one for the ViewModel as a stream of presentation events. This way, a View would only emit events to its ViewModel, and the ViewModel would only emit events to the Controller. A uni-directional data flow at last.

Finally, to wrap up things and make my new spicy code play nicely with Android, I built a small stateless function which takes any number of UiViews and a ViewModel which controls them all and performs some boiler plate to set everything up. This createComponent function will inflate all Views, open the ViewModel state stream and UiView event stream, and register to a LifecycleObserver, which will dispose the stream and teardown the views when the lifecycle is destroyed. Much nicer.

I'm not the best speaker or the best writer - and can't really convey these ideas easily over a post. You may just be better off looking at the source code itself to gain a better understanding of what I'm trying to do. The buggy code for the new and hopefully improved UiViewModel can be found here. Here is the new UiView code, and my simple component wrapper.

I'll be updating all apps to follow this new architecture and eagerly await the day that Jetpack Compose releases. This new architecture should lead to fewer bugs for all the existing pyamsoft Android applications, as well as speed up the development process of my new application that is still a work in progress... but that's for another day.

========================
Follow pyamsoft around the Web for updates and announcements about the newest applications!
Like what I do?

Send me an email at: pyam.soft@gmail.com
Or find me online at: https://pyamsoft.blogspot.com

Follow my Facebook Page

See my code on GitHub  
=========================