Using LiveData in Coinverse pc — Ned Scher , Waterfall at Yosemite National Park (2015) Code in the wild is always better to learn from than slide samples. Here I’ll be sharing code from Coinverse’s Open App to showcase a live Unidirectional Data Flow (UDF) pattern which makes the app more readable, easier to develop for, and debug. The previous post, , outlines the strategy for loading a newsfeed and removing adjacent ads using UDF. This post shares both a basic live example, and a more complex structure to return multiple processes in one method using . Android Unidirectional Data Flow with LiveData MediatorLiveData By downloading the , you can run the code to explore, listen on-the-go, and watch the newsfeed of audiocast and video content. For access to features requiring authentication such as saving and dismissing content, you’ll need to implement the to set up a Firebase project outlined in . GitHub project 5 easy-ish steps Coinverse Open App — Set Up pc—My Mom’s Kenya safari (1980s) Brief UDF OverviewView Events, State, and Effects — Actions created by the user that effect the data displayed, or system level events like the app starting or stopping Events — Info that displays or helps determine a view, created in the business layer of logic State — One time user interface occurrences like navigation, dialogs, and toasts created by the business layer of logic Effects LiveData provides a simple, Android lifecycle aware, toolset to handle automatically updating data for events. LiveData Loading, Content, Error Network Requests Network requests returned with states wrapped in LiveData objects ensure that the ViewModel business logic layer accounts for the loading, successful content loaded, and error states. LCE { () ( packet: T) : Lce<T>() ( packet: T) : Lce<T>() } sealed < > class Lce T < > : < > class Loading T Lce T data < > class Content T val data < > class Error T val View Event, State, and Effect — ContentSelected ContentSelected view event In this first case when video content is selected in the Adapter it triggers a view event in the The event is processed in the . ContentSelected ContentFragment.kt. ContentViewModel.kt (...): PagedListAdapter<Content, ContentAdapter.ViewHolder>(DIFF_CALLBACK) { contentSelected: LiveData<Event<ContentSelected>> () = _contentSelected _contentSelected = MutableLiveData<Event<ContentSelected>>() = OnClickListener { view -> (view.id) { preview, contentTypeLogo -> _contentSelected.value = Event(ContentSelected(view.getTag(ADAPTER_POSITION_KEY) , content)) } } } class ContentAdapter val get private val private fun createOnClickListener (content: ) Content when as Int () { viewEvent: LiveData<Event<ContentViewEvent>> () = _viewEvent _viewEvent = MutableLiveData<Event<ContentViewEvent>>() { adapter = ContentAdapter(contentViewModel, _viewEvent).apply { .contentSelected.observe(viewLifecycleOwner, EventObserver { contentSelected -> _viewEvent.value = Event(ContentSelected( getAdapterPosition(contentSelected.position), contentSelected.content)) }) } { .onResume() viewEvent.observe(viewLifecycleOwner, EventObserver { event -> contentViewModel.processEvent(event) }) } : class ContentFragment Fragment private val get private val private fun initAdapters () this override fun onResume () super Because the is a video, the ViewModel updates the LiveData value of the view state with the object containing a video url. contentType contentToPlay FeedViewState ContentToPlay (application: Application) : AndroidViewModel(application) { { (event) { ContentViewEvent.ContentSelected -> { contentSelected = ContentViewEvent.ContentSelected(event.position, event.content) (contentSelected.content.contentType) { YOUTUBE -> { setContentLoadingStatus(contentSelected.content.id, View.GONE) _viewEffect.value = Event(NotifyItemChanged(contentSelected.position)) _feedViewState.value = _feedViewState.value?.copy(contentToPlay = MutableLiveData<Event<ContentResult.ContentToPlay>>().apply { .value = Event(ContentResult.ContentToPlay( contentSelected.position, contentSelected.content, , )) }) } } } } } } class ContentViewModel fun processEvent (event: ) ContentViewEvent when is val when this "" "" ( contentToPlay: LiveData<Event<ContentResult.ContentToPlay>>, ...) data class FeedViewState val ( position: , content: Content, filePath: String?, errorMessage: String) : Parcelable {...} data class ContentToPlay var Int var var val The Fragment observes the value change, and launches the Fragment to play the video. FeedViewState contentToPlay () { { contentViewModel.feedViewState.observe(viewLifecycleOwner, Observer { viewState -> viewState.contentToPlay.observe(viewLifecycleOwner, EventObserver { contentToPlay -> (feedType) { MAIN, DISMISSED -> (childFragmentManager.findFragmentByTag(CONTENT_DIALOG_FRAGMENT_TAG) == ) ContentDialogFragment().newInstance(Bundle().apply { putParcelable(CONTENT_TO_PLAY_KEY, contentToPlay) }).show(childFragmentManager, CONTENT_DIALOG_FRAGMENT_TAG) } }) }) } } : class ContentFragment Fragment private fun observeViewState () when if null In addition to the view state being updated in the ViewModel above, a view effect, is added and observed in the Fragment to update the RecyclerView cell that has been selected. NotifyItemChanged () { { contentViewModel.viewEffect.observe(viewLifecycleOwner, EventObserver { effect -> (effect) { ContentViewEffect.NotifyItemChanged -> adapter.notifyItemChanged(effect.position) } }) } } : class ContentFragment Fragment private fun observeViewEffects () when is MediatorLiveData — PlayerLoad PlayerLoad view event In the first case the event is straightforward because there is a view event and one piece of data, the object, returned in the view state. The event created from the , that retrieves the required information to play an audiocast, is more complicated because it requires two network requests. ContentSelected ContentToPlay PlayerLoad AudioFragment.kt () { viewEvent: LiveData<Event<ContentViewEvent>> () = _viewEvent _viewEvent = MutableLiveData<Event<ContentViewEvent>>() { ... contentToPlay = arguments!!.getParcelable(CONTENT_TO_PLAY_KEY)!! (savedInstanceState == ) _viewEvent.value = Event(ContentViewEvent.PlayerLoad( contentToPlay.content.id, contentToPlay.filePath!!, contentToPlay.content.previewImage)) } } : class AudioFragment Fragment private val get private val override fun onCreate (savedInstanceState: ?) Bundle if null (application: Application) : AndroidViewModel(application) { playerViewState: LiveData<PlayerViewState> () = _playerViewState _playerViewState = MutableLiveData<PlayerViewState>() { (event) { ContentViewEvent.PlayerLoad -> _playerViewState.value = PlayerViewState(getContentPlayer( event.contentId, event.filePath, event.previewImageUrl)) } } } class ContentViewModel val get private val fun processEvent (event: ) ContentViewEvent when is The returned requires retrieving information from two sources for both the file to play audio, and a converted Bitmap image to display in the notification player. PlayerViewState mp3 ( contentPlayer: LiveData<Event<ContentResult.ContentPlayer>>) data class PlayerViewState val { ( uri: Uri, image: ByteArray, errorMessage: String): ContentResult(), Parcelable {...} } sealed class ContentResult data class ContentPlayer val val val The ViewModel could achieve getting both pieces of information by making one network request, waiting for it to return successfully, then make the second request. However, this creates more complicated and nested code. In order for the ViewModel to observe LiveData from the network requests it must be observed by the UI layer in the AudioFragment. The first network request must be observed, then within that method, the second request needs to be observed. This creates nested code within the ContentViewModel as well which gets ugly. LCE A Better Solution With MediatorLiveData A better solution is to return one LiveData object that contains both the and image using takes in two LiveData objects, returning the , and returning the image. only returns the LiveData once each LiveData item passed into the method has been populated with info, indicated by an emitted boolean in the code below. mp3 Bitmap MediatorLiveData . combineLiveData getContentUri mp3 bitmapToByteArray Bitmap combineLiveData ContentPlayer (application: Application) : AndroidViewModel(application) { = getContentUri(contentId, filePath).combineLiveData(bitmapToByteArray(imageUrl)) { a, b -> Event(ContentResult.ContentPlayer(a.peekEvent().uri, b.peekEvent().image, getLiveDataErrors(a, b) )) } -> T) = MediatorLiveData<T>().also { result -> source1emitted = source2emitted = mergeF = { source1Value = .value source2Value = other.value (source1emitted && source2emitted) result.value = onChange.invoke(source1Value!!, source2Value!!) } result.addSource( ) { source1emitted = ; mergeF.invoke() } result.addSource(other) { source2emitted = ; mergeF.invoke() } } = switchMap(repository.getContentUri(contentId, filePath)) { lce -> (lce) { Lce.Loading -> MutableLiveData() Lce.Content -> MutableLiveData<Event<ContentResult.ContentUri>>().apply { value = Event(ContentResult.ContentUri(lce.packet.uri, )) } Lce.Error -> { Crashlytics.log(Log.ERROR, LOG_TAG, lce.packet.errorMessage) MutableLiveData() } } } = liveData { emitSource(switchMap(repository.bitmapToByteArray(url)) { lce -> (lce) { Lce.Loading -> liveData {} Lce.Content -> liveData { emit(Event(ContentResult.ContentBitmap(lce.packet.image, lce.packet.errorMessage))) } Lce.Error -> liveData { Crashlytics.log(Log.WARN, LOG_TAG, ) } } }) } = a.peekEvent().errorMessage.apply { ( .isNotEmpty()) }.apply { b.peekEvent().errorMessage.also { (it.isNotEmpty()) .plus( + it) } } } class ContentViewModel private fun getContentPlayer (contentId: , filePath: , imageUrl: ) String String String /** * Sets the value to the result of a function that is called when both `LiveData`s have data * or when they receive updates after that. */ private LiveData . fun <T, A, B> <A> combineLiveData (other: < >, onChange: ( , B) LiveData B A var false var false val val this val if this true true private fun getContentUri (contentId: , filePath: ) String String when is is "" is private fun bitmapToByteArray (url: ) String when is is is "bitmapToByteArray error or null - " ${lce.packet.errorMessage} private fun getLiveDataErrors (a: < . >, b: < . >) Event ContentResult ContentUri Event ContentResult ContentBitmap if this this if this " " This allows for each of the two methods requiring a network request to handle their LCEs separately, and for one LiveData object to be observed from the in the AudioFragment. ContentPlayer () { contentToPlay: ContentResult.ContentToPlay contentViewModel: ContentViewModel { contentViewModel.playerViewState.observe(viewLifecycleOwner, Observer { viewState -> viewState?.contentPlayer?.observe(viewLifecycleOwner, EventObserver { contentPlayer -> }) }) } } : class AudioFragment Fragment private lateinit var private lateinit var private fun observeViewState () //Launch audiocast! In addition to the live code I’ve created notes on LiveData and the UDF pattern including documentation notes, videos, and samples. I’m the creator of Coinverse. the app on Google Play, and me for more on Android engineering! Adam Hurwitz, Download follow