Another way of using LiveData with Jetpack Compose

Citizen Warwick
4 min readMar 2, 2020

--

Jetpack Compose is coming on leaps and bounds and we’ve had a glimpse of what LiveData usage within compose may look like but nothing officially recommended as the “go to” standard.

Jetpack Compose provides a very convenient way of hooking into the compose lifecycle with a few callbacks the first of which is onActive { }.

Composition lifecycle callbacks

In our compose functions (those marked with @Composable we can hook into this composition quite simply by doing the following.

In the example our root composition starts with setContent (if you are not familiar with compose yet, this is where we can write our declarative UI). Inside this block, we can put our onActive composition lifecycle callback to add a block of code that will be only executed once per composition. Within the onActive block we can specify another block onDispose which will get called when this composition is invalidated, therefore providing convenient hooks for callback/subscription based API’s such as LiveData<T>.

LiveData integration example

Given the basics out of the way the next code snippet shows a usage pattern of a custom function observe that makes use of compose lifecycle callbacks which accepts a LiveData as its first argument and has a convenient callback onResult where the live data result will be delivered (effectively wrapping the observe behaviour of live data).

The example is not small, but requires some explanation. First up we remember a model HelloModel. This model is annotated with @Model which is composes way of specifying observable data that when changed, can cause a recomposition.

Next up on our observe block which we give the result of a function call to getHello that returns a LiveData<HelloResult>. (the implementation of the observe function is where the magic happens which I will show and break down later).

As mentioned we specify another block inside the observe call onResult { }. Here we can take the result of the live data call and pass it to a property on our model with hello.state = result, where result is a property available in only the onResult block.

The final code block in setContent is where we decide which message to show depending on the value ofHelloModel::state. It is a simple when(state){} block when that will show an appropriate message depending on the state being Pending , Hello or Error. (Currently no error could occur with this code example, however for brevity its here to give you an idea of how you could handle an error).

Under the hood of the observe function

Inside this custom observe function is where all the magic happens. Where we hook into the composition using onActive and onDispose.

The function is not as simple as it could be since I plan to expand it if nothing more comes along hence the usage of receivers for scoped functionality.

So what is going on here? Firstly the observe function takes an argument data: LiveData<T> which is the live data we wish to load. secondly we have a trailing lambda block: ObserveScope<T>.() -> Unit. which is effectively is a delegate for our live data observer (which we never need to use with this setup).

The function does some setup and calls context.onStartBlock() giving integrations the chance to hook into the moment just before the LiveData is observed with onStart {} (maybe for showing a loading spinner or something).

Moving on we create an observer and in the onChanged callback we call context.onResultBlock which ultimately calls onResult {} which we saw previously in the usage sample.

Next we observeForever(observer) which kicks off the live data observation. finally we declare a onDispose block and ensure we remove observers. The rest of the code is mostly specific to my exploration and usage of this pattern.

Looking back at the observe function you may have noticed that the data: LiveData<T> argument is annotated with @Pivotal. What this does is tell the compose compiler that this argument should act as unique ID for this block and if a recomposition occurs and this unique ID changes then this part becomes invalid and should be recomposed causing the onDispose block to be called and onActive to be called again. Without compose knowing its unique this would never happen.

Handling errors and retry

So I mentioned that error handling is not in the usage example snippet (well handling is there just not throwing an error and retrying), so the only way I have found out so far to retry a composition is by using a convenient composable called Recompose which I will explain with another example that builds on the previous example.

Here we wrap our code with aRecompose block which gives us a lambda callback we call recompose which we call later if an error occurs as the action of a button click. The code mimics an error with a field doError firstly set to true which causes loading with an error. Then in our retry Button onClick callback we set doError to false so when we call recompose() Our live data will be loaded again but this time with a successful result HelloResult.Hello.

An important point to note is we check hello.state != result before setting the state on the model as I found it causes infinite recomposition.

I don’t know if this is the best way, to use Recompose in this manner, what do you think?

Conclusion

These are my most recent dabbling with Jetpack Compose and LiveData integration, not 100% sure if its right however that was partly the point of this article, to get my thoughts out there and what I have found out so far.

I am using this method in my little compose app and it works well so far https://github.com/fluxtah/memset

What do you think? Have you found a better way of LiveData integration that ticks all the boxes such as error handling and reloading (recomposing?) I am interested to hear about this in the comments or reach out to me, maybe join the Jetpack Compose slack channel and you can find me there! :)

--

--

Citizen Warwick
Citizen Warwick

Written by Citizen Warwick

World Citizen, Musician and Programmer

No responses yet