Android Unidirectional Flow with LiveData

1,285 reads

@ adamhurwitz Adam Hurwitz Creator of Coinverse - The 1st Crypto News Audiocast App @ bit.ly/play-coin

Android Unidirectional Data Flow with LiveData

Improving Coinverse’s Performance and Structure

pc — Ned Scher, Waterfall at Yosemite National Park

The Unidirectional Data Flow (UDF) pattern has improved the usability and performance of Coinverse since the first beta launched in February. Coinverse is the first app creating audiocasts covering technology and news in cryptocurrency. Upgrades using UDF include more efficient newsfeed creation, removal of adjacent native ads, and faster audiocast loading.

reactions

The UDF pattern organizes the app into three main areas, view state, events, and effects ensuring the app is modularized and reliable. I learned of the UDF pattern from episode 148 of the Fragmented podcast, Evolving Android architectures (Part 1) with Kaushik Gopal at Instacart and Donn Felker. On first listen it was interesting, but a large overhaul. UDF became compelling as I worked on fixing bugs and realized how complex various flows had become.

reactions

The examples of UDF I’ve seen thus far have been with Rx. Rx a powerful and customizable tool for creating streams of data that can be observed in real-time. However, LiveData provides the same benefit of observing state changes and on top of that is simple, directly integrates with Android’s architecture components, and handles Android’s lifecycle events by default. If unfamiliar, check out Google’s Jose Alcérreca’s talk on LiveData. To try out UDF I refactored the newsfeed flows in Coinverse as we’ll walkthrough below.

reactions

UDF Background

UDF pattern, a.k.a. Unidirectional Data or State Flow was originally popularized in web development with Facebook’s React and Readux state management, and Flux UI libraries. Early Android experimentation can be found from Brian Egan and Guillaume Lung at SoundCloud in 2015. Jake Wharton’s talks on Rx educated developers on reactive programming which are fundamental to UDF.

reactions

Growing Number of Android Apps Adopting

To name a few…

reactions

App companies adopting UDF

Model View View Model — MVVM

Gem Lake, Yosemite Emigrant Wilderness Trail

For the first iteration of Coinverse I used the Model View View Model (MVVM) approach. MVVM separates the UI from the business logic, improving readability and organization. However, as an app grows with MVVM it becomes a lake of data. Information flows in, out, and around at many points via Activities and Fragments with Data Binding, ViewModels, and Repositories. This adds complexity for keeping track of logic, debugging, and testing, which requires mocking many components.

reactions

UDF Advantages

pc — Ned Scher, Waterfall at Yosemite National Park

UDF is a waterfall, information flows in one direction through a single source providing many benefits.

reactions

One point of entry for streams — The UI and business logic interact through single points of entry.

Control UI involving async events — Know exactly when and where things begin and finish

Debug issues — Easy to follow sequence of events and identify errors

Streamlined tests — The majority of logic is contained in the ViewModel requiring less mocking.

LiveData

LiveData provides a straightforward approach to implementing UDF.

reactions

Lifecycle aware

Can emit multiple or single events

Concise code

Quick to implement

View State, Events, and Effects

ViewState

Coinverse’s main newsfeed

View state is responsible for holding the final view’s persisted data. This entails all of the content shown to a user on a screen, including information about the content such as enabled statuses.

reactions

Looking at Coinverse’s main newsfeed above, examples of view state include the contents of the toolbar, what timeframe and the feedtype of feed to display, as well as the contentList to populate the feed with.

reactions

Events

ContentSelected(…)

View events consist of both user interface and system initiated actions. UI actions include button presses and text input, whereas system actions might be Android lifecycle events and screen rotation.

reactions

In the case of The Coinbase Blog’s content selected above a view event is created, ContentSelected. The event will share information with the business layer to initiate the retrieval of the audiocast selected.

reactions

Effects

ContentSwiped(…)

View effects are one time UI occurrences that don’t persist. Effects include navigation, dialogs, and toasts. Effects are created by the business layer to initiate changes in the UI.

reactions

When the CCN item above is swiped right, the business layer adds a saved label to the content. The business layer sends an effect, ContentSwiped, informing the UI of the change in the content’s label. The UI can then remove the content from the main newsfeed.

reactions

App Structure

Let’s understand how the one-way flow of data is structured. The View handles all UI and system level actions stored in a single stream. The stream is sent to the ViewModel that receives the actions and handles them accordingly in the business logic.

reactions

The ViewModel is the source of truth for the view state and creates any necessary effects. The ViewModel also handles requests to the data Repository layer, managing the results loading, success (content), and error states returned from the Repository with an Lce object (more on Lce's below).

reactions

Both the state and effects are observed by the View, updating any changes from the ViewModel in real-time.

reactions

Implementation

mainfeed loading

We’ll use Coinverse’s main newsfeed loading as our example for how to implement UDF.

reactions

Step 1 of 6 — Define Models

View state — Stored as a LiveData object in the ViewModel, storing the contentList of LiveData type

Stored as a LiveData object in the ViewModel, storing the contentList of LiveData type View event and effect — Use Kotlin’s Sealed class to pass one time events

The view state uses LiveData because it’s important the data is immutable vs. MutableLiveData. Otherwise, the flow of data would not be unidirectional, and the state could be changed in many places.

reactions

View events and effects are not persisted in the ViewModel. A Sealed class, like an Enum, but on a class level, is used to pass information. Sealed classes define a parent and child class with or without data. TheScreenLoad event is a data class with data about what the ViewModel should load. Whereas the UpdateAds effect is a class without data telling the view to update the ads in the newsfeed.

reactions

Step 2 of 6 — Pass events to ViewModel

In this example, when the system action of onCreate occurs, a ScreenLoad event is added to the stream of view events and sent from the Fragment to the ViewModel to start creating the main feed.

reactions

All of the events created in the View / Fragment are added to a LiveData object _viewEvent, a MutableLiveData object which updates the immutable LiveData object. I’m using the pattern of passing all of the events in onResume based on Kaushik’s sample.

reactions

The LiveData stores data wrapped in an Event. As explained by Jose in his LiveData post about events, events ensure a single unique object is added to a stream. This avoids the accidental creation of multiple objects for a single action.

reactions

Step 3 of 6 — Process events

mainfeed loaded

The ViewModel receives incoming events, handling each event in a when statement based on the type of Sealed ViewEvent class. For ScreenLoad, the entireViewState is updated with the required data. To populate the newsfeed a request to the Repository with getMainFeed is made.

reactions

Update State Value

reactions

In cases where an attribute of the ViewState needs to be updated rather than the entire ViewState, Kotlin’s shallow copy function is useful.

reactions

Step 4 of 6 — Manage Network Requests with LCE Pattern

To manage network requests, Kaushik introduces the Lce Sealed class object with three states, loading, content, and error. The content state represents a successful request.

reactions

A Sealed class is also useful for returning different types of results.

reactions

getMainFeed’s network request shares send the Lce states to the ViewModel via the LiveData stream. The PagedListResult class can be passed into the Lce for both the content and error states. The ViewModel will then manage each state appropriately.

reactions

Step 5 of 6 — Handle LCE States

mainfeed error

The gif shows something has gone awry. We’ll see how the error is handled in the ViewModel.

reactions

UDF has streamlined both methods of requesting new content from the network and retrieving the updated content from the Room database. Prior to using UDF, Coinverse called two repository methods separately to populate the main newsfeed.

reactions

When the feed is loading the existing Room database content is returned so that the user is not staring at an empty screen. For the successful Content case, the updated content from Room is returned. For errors requesting new content, the existing Room content is also displayed similar to theLoading case. In the error above, a SnackBar view effect is passed to the Fragment to display the error message.

reactions

The ViewModel observes each Lce state with a LiveData SwitchMap. The SwitchMap passes in one LiveData object and returns a new and different LiveData object that is saved to the view state.

reactions

Like all LiveData, a SwitchMap must be observed in the view in order for the value to be emitted within the map inside the ViewModel.

reactions

Step 6 of 6 — Observe State Change!

Now the view state may be observed when an update occurs. The view effects are observed in the same way.

reactions

Bonus — Removing Adjacent Ads

auto adjacent ads detection

In addition to a streamlined newsfeed above, UDF has improved how Twitter’s native MoPub ads are shown in the newsfeed. MoPub’s MoPubRecyclerAdapter does not have a built-in approach to avoid adjacent ads from showing. Content can be swiped to be saved or dismissed, eventually causing two ads to appear next to each other. Prior to UDF, this was handled with a manual swipe-to-refresh by the user.

reactions

With UDF there is a contentLabeled view state. When the status of the view state changes, meaning an item is labeled to be removed from the main feed, a check for adjacent ads is made. If removing the content creates adjacent ads, the ads are automatically refreshed.

reactions

Trade-Offs

Using LiveData for the Unidirectional Data Flow has been great, but it’s not perfect.

reactions

LiveData is only applicable for logic impacting the UI. For non-UI logic LiveData will not be observed since it requires lifecycle to be passed in. For these instances, Kotlin coroutines or Rx may be used. If no UI is involved then an even better solution might be to offload the logic completely to the backend with Firebase Cloud Functions.

reactions

There’s not as much customization with LiveData for things like threading. With this year’s latest Google I/O updates coroutines appear to easily integrate with LiveData offering more customization.

reactions

Coinverse Next Steps

Unidirectional Data Flow — Expanding to the rest of Coinverse’s app

Expanding to the rest of Coinverse’s app JUnit testing — Now that the majority of the newsfeed logic is modularized in the ViewModel, JUnit testing will be easier with less mocking of components.

— Now that the majority of the newsfeed logic is modularized in the ViewModel, JUnit testing will be easier with less mocking of components. Kotlin coroutines — Exploring integration with LiveData

Resources

I’d love your feedback on the Coinverse beta!

Follow me to be updated on a Unidirectional Data Flow sample app and more.

reactions

A big thanks to Cristián Gomez and Carlos Daniel of the Medellín Android MeetUp for organizing the talk! If you are in Medellín I recommend stopping by their MeetUp.

reactions

Tags