Based on amazing feedback from the Android community and taking advantage of Kotlin’s natural language features, the Android Unidirectional Data Flow (UDF) with LiveData 2.0 improves and simplifies upon the original pattern. Version 1.0 improved the efficiency of Coinverse’s newsfeed, removed adjacent native ads, and made audiocast loading faster.

Previous:

Sample:

Recap

The first 13 min. of my DroidCon San Francisco talk cover the UDF 2.0 pattern. See slides 10–25.

JUnit 5 Testing: Android Unidirectional Data Flow with LiveData (DroidCon San Francisco 2019)

UDF Strengths 💪

Linear flow of app logic

Control UI & async events

Pre-defined test structure

Faster debugging

Structure

View state — Final persisted data of a screen displayed to a user

— Final persisted data of a screen displayed to a user View events — State changes initiated by the user or Android lifecycle

— State changes initiated by the user or Android lifecycle View effects — One time UI occurrences that don’t persist like navigation or dialogs

UDF Structure

Improvements

View events — Create hard contract between the view and ViewModel .

Create hard contract between the view and . View effects — Enable multiple + simultaneous effects with LiveData attributes and simplify code with custom extension functions.

Enable multiple + simultaneous effects with attributes and simplify code with custom extension functions. Repository — Improve data transformations and flatten request callbacks with Coroutines.

— Improve data transformations and flatten request callbacks with Coroutines. ViewModel — Simplify data requests with Kotlin Coroutines Flow .

View Events

‘FeedLoad’ view event fired in ‘onCreate’ of Android lifecycle

During the 2019 Android Developer Summit I was able to pick Yigit Boyar’s brain briefly on this pattern. Yigit recommended to create a strong contract between the view and ViewModel events. In 1.0 the events are observed as a single stream of LiveData , and processed in the ViewModel .

Create Contract Between the View and ViewModel

1.0

With the stream of events, if an event is not processed in the ViewModel , the app still compiles and the code will not perform as expected.

Pass events stream to ViewModel .

2. Process events in ViewModel with processEvent .

2.0

Instead of loosely processing the View events in the ViewModel with a LiveData steam, a contract can be defined with an Interface between the View and ViewModel . This ensures all expected events are processed. If the ViewModel does not override an event from the Interface a Lint error will throw and the code will not compile.

Define view events.

2. Send events to ViewModel with interface .

3. Implement events interface in ViewModel .

View Effects

‘ContentSwipedEffect’ for navigating item off of the feed after it has been swiped

Enable Multiple + Simultaneous Effects

Another point from Yigit regarding 1.0 is the ViewEffect is only able to send one event at a time from the ViewModel to the UI. For most cases this may work. However, with more features added in the future, numerous effects may need to process simultaneously.

1.0

Create view effect types in order to use parent Sealed class effect to pass state change from the ViewModel to the view.

2. Send view effect to the view by updating the state of the view effect LiveData value.

2.0

In order to handle multiple + simultaneous effects, the ViewEffect model is refactored to have LiveData attributes for each effect type. The same as before, the effect change is updated in the ViewModel as a LiveData variable.

Create view effect with LiveData attributes for each type.

2. Send the view effect to the view by creating the view effect state, ContentEffects , and saving the LiveData value to the view effect attribute updateAds . This ensures the state change will be observed in the view and update the ads accordingly. Because each attribute is of LiveData type, multiple effects can be saved at the same time.

Use Custom Extension Functions

Once the view effect state ContentEffects is initialized in the ViewModel , in order to update the existing attributes we use Kotlin’s copy extension function to preserve the current state of the ContentEffects ' attributes.

To improve the ViewModel readability a custom send extension function has been created to handle this process for each view effect attribute.

2. Then use the send extension to pass the UpdateAdsEffect to the view. We are using the send extension because the view effect state object has already been initialized, and we just need to update it’s attribute.

Repository

Photo by Jez Timms on Unsplash

In 1.0 the Repository returns LiveData which is processed in the ViewModel , then updated in the final persisted view. Using LiveData between the ViewModel and view is great because it is tied to the view lifecycle and the view data is observed when the state updates.

LiveData Can be Problematic in the Repository

Data processing is complicated because LiveData must be observed in the view vs. Coroutine Flows which can be returned anywhere.

must be observed in the view vs. Coroutine which can be returned anywhere. Transforming LiveData requires either a switchMap or using MediatorLiveData .

requires either a or using . LiveData creates unnecessary syntax in the ViewModel , which we’ll explore in the last section.

Improve Data Transformations with Flow

Using Kotlin’s Coroutine Flow we can return data directly in the Repository, and apply transformations if needed before returning the final data to the ViewModel .

1.0

Make a data request for the main feed, parse the response, and return LiveData to the ViewModel .

2.0

To properly handle the lifecycle of the Flow , it’s important to launch the original call from the ViewModel to make sure it’s tied to the ViewModel lifecycle.

Flatten Request Callbacks

Notice above how 2.0’s Repository is more readable without the multiple layers of nesting required when using callbacks. In order to remove callbacks we can convert these requests to Coroutines.

There are two types of network requests to convert.

One-time requests

Realtime updates

One-time requests

LiveData with Coroutines and Flow (Android Dev Summit '19): Convert callback requests to coroutines — 14:50

One time requests are Rest API calls that return a single Json response. Firebase Firestore’s get() method is an example of a one time request.

Using Rosário Pereira Fernandes implementation, and Joe Birch’s strategy, we can use await as seen above to cleanly return the asynchronous result. As Rosário links to, the coroutines-android and coroutines-play-services libraries are required to implement the await extension function to remove the callback nesting.

Realtime updates

Realtime requests involve creating a constant stream of data with a Websocket in order receive data as it is transmitted from the network. Firebase Firestore’s addSnapshotListener is an example of a realtime request.

As outlined in this StackOverflow, I built the custom awaitRealtime extension function in order to return the results asynchronously.

The extension function awaitRealtime has checks including verifying the state of the continuation in order to see whether it is in isActive state.

has checks including verifying the state of the in order to see whether it is in state. This is important because the function is called when the user's main feed of content is updated either by a lifecycle event, refreshing the feed manually, or removing content from their feed. Without this check there will be a crash.

In order to handle errors the try / catch pattern is used in the Repository.

ViewModel

Photo by Bruno Perrin on Unsplash

In 2.0, to simplify handling the data response from the Repository, the ViewModel uses Flows . Instead of LiveData 's switchMap we call Flow ’s collect and emit the LiveData to the state change.

Simplify Data Requests with Kotlin Coroutines’ Flow

1.0

2.0

There is no longer the need to create switchMaps and MutableLiveData instances in order to process and return the data in the ViewModel .

and instances in order to process and return the data in the . The liveData Coroutine is used to return LiveData to update the view state with less boilerplate code.

Next:

Resources

Check out Coinverse on the Play Store: