id: 161 View:article
Tutorials  Architecture
images/intro/tutorials/architecture/RobustLiveDataTask_300x300.png

The ViewModel - your single source of truth

Android developers will have heard about the not-so-quiet revolution introduced at Google I/O 2018 in the form of "JetPack". This is an awesome set of tools, software components and just plain know-how which Google put together to let everyone use the best-practice they've evolved into using themselves. Sure, you can continue with your current frameworks and libraries, it's just that now, zillions of the wheels you might be tempted to reinvent are all laid out for you, and in a way that makes using them a really smart choice.

Jetpack covers UI navigation, tasks, db etc and you can read more about it here. What caught my attention was the way an old problem I've covered before can be tackled, and since you don't need an oracle to see where languages on Android are headed, I chose Kotlin rather than Java. The new killer component is called the ViewModel, and it's purpose it to hold non-UI data which lives independently from the usual app lifecycle. Even better - it can run methods in this new "outside the Activity" place, which means we have a new way to run background tasks. ViewModel predates JetPack, but is considered one of it's central components now.

What's presented here is a way to keep the UI up to date even when the app is backgrounded. There are plenty of ViewModel tutorials around, such as here, here and here, so I'll skim over the basics and focus on the backgrounded UI part, which needs some care to get right.

The source for this project is available on GitHub, and if you just want to run it, allow unknown sources on your Android device and click here.

Another way to run background code

The problem the app tries to solve is easy enough to define: start a finite task which can only be terminated when the user quits the app, or the task terminates normally. The task in practice might be a download, a song played, or even these days a chunk of AI training data sent to the cloud. The "placeholder" task here is just an incrementing progress bar. So, when it's started there should be nothing the user can do to stop it short of forcing the app to quit, like pressing the back button whilst it's running in the foreground. That means it continues when the phone is rotated, when it's backgrounded with the home button, when a call comes in etc. It's a Robust ViewModel Task ;-)

The requirements:

  • Once started, the task must complete unless the user presses the back button whilst it's running
  • Rotating the phone or backgrounding the app must not stop or pause the task
  • An indicator that the task is running must appear in the status bar irrespective of the apps visibility
  • Only at app startup should the begin button appear
  • The restart button must only appear when the task has completed

These requirements are a handy way to illustrate the way the state of the UI can be managed with the ViewModel. Here's what it looks like - note the status bar indicator when the task is running - that's an important requirement of the Android guidelines which say you must always keep the user notified that your app is doing something in the background, even when it's not visible:

RobustLiveDataTask 300x522

ViewModel

The idea of abstracting out the data an app manages from how it's presented isn't new by a long stretch. Many frameworks have been implemented on Android with various permutations of the words "View", "Model" and sometimes "Controller", and Google's Jetpack is no exception ;-) It ended up being known as the MVVM design - Model-View-ViewModel. The ViewModel is an actual class you implement which holds the data, and hence the state of the UI. Ours looks like this:

MainViewModel.kt

package com.otamate.android.robustlivedatatask

import ...

class MainViewModel(application: Application) : AndroidViewModel(application) {
    companion object {
        private const val TAG = "MainViewModel"
        const val ITERATIONS = 100
        private const val SPEED = 50L
    }

    data class ViewStateData (
        var isBegun: Boolean = false,
        var isInProgress: Boolean = false,
        var isFinished: Boolean = false
    )

    data class ProgressData (
        var progress: Int = 0
    )

    private val liveDataViewStateData: MutableLiveData = MutableLiveData()
    private val liveDataProgressData: MutableLiveData = MutableLiveData()

    init {
        liveDataViewStateData.value = ViewStateData()
        liveDataProgressData.value = ProgressData()
    }
    ...
}

Wait ... this class extends AndroidViewModel, and not the plain ViewModel. Well, since AndroidViewModel extends ViewModel we get everything it provides anyway. However, we need to do this because whilst the UI is now separated, it still lives in the Views of your Activities and Fragments. It turns out we need to interact with the system for certain "global" UI elements from the ViewModel itself.

The way the ViewModel works is to set itself up as an observable from the UI. In other words, when it's data changes, it tells the UI which has set itself up as a listener for these events. However, with JetPack we wrap these data packets up in LiveData objects, which are lifecycle aware. This has been done to eliminate a ton of problems faced previously , such as keeping track of which Activity is active, memory/context leaks plus a whole heap of boilerplate code just for this housekeeping. In short, LiveData is lifecycle aware, so knows how to deal with all this, and is efficient too in the sense that it won't send these UI update events if there's no UI visible to update. That's all well and good, but what about our situation here, where we want to update the system, in the form of the status bar, even when the app is backgrounded?

Recall that the ViewModel is the "pure" data model which has no access to the context of your app, and no - don't even think of sending your context to it as a way to bypass this restriction. If you did, you're back in the world of hurt that is memory leaks and duplicated contexts. Fortunately, Google saw this so gave us AndrioidViewModel, which means you get ViewModel but with the ability to use the app context. It's OK for situations such as this, where it's the system UI, but remember to do it responsibly only when needed and never touch any of your own app's views or contexts from it.

LiveData

Notice the regular ViewStateData and ProgressData classes, which just hold the primitive booleans and ints representing the UI state at any time, are wrapped by MutableLiveData objects, which is what the observers in the UI listener code need. Once set up, any changes to their contents magically get sent to the UI. Here's how our ViewModel actually processes things:

MainViewModel.kt

// Set the ViewState
fun setViewStateData(newViewStateData: ViewStateData) {
    Log.d(TAG, "setViewStateData: " + newViewStateData)

    if (!getViewStateData().isBegun && newViewStateData.isInProgress) {
        newViewStateData.isBegun = true
    }

    if (!getViewStateData().isInProgress && newViewStateData.isInProgress) {
        start()
    }
    liveDataViewStateData.value = newViewStateData
}

fun start() {

    // Do the actual work in a thread for performance
    thread(start = true) {
        var intent = Intent()
        val app: Application = getApplication()

        Log.d(TAG, "Starting")

        intent.action = MainActivity.SHOW_STATUS_BAR_ICON
        LocalBroadcastManager.getInstance(app).sendBroadcast(intent)

        while (getProgressData().progress < ITERATIONS) {
            liveDataProgressData.postValue(getProgressData().copy(
                progress = getProgressData().progress + 1))
            Thread.sleep(SPEED)
        }

        intent = Intent()
        intent.action = MainActivity.HIDE_STATUS_BAR_ICON
        LocalBroadcastManager.getInstance(app).sendBroadcast(intent)

        liveDataViewStateData.postValue(getViewStateData().copy(
            isFinished = true, isInProgress = false))

        Log.d(TAG, "Done")
    }
}

The progress counter just gets incremented normally, blissfully unaware it's every change is being picked up by the UI and shown immediately, as are the other toggles holding whether to show the Begin button, etc. Notice the getApplication() call - this is needed to set up the status bar notification icon, but would fail if we weren't using AndroidViewModel, as described earlier.

The UI

So how exactly are these data changes picked up and displayed? Here's the code where these listeners are set up:

MainActivity.kt

class MainActivity : AppCompatActivity() {

    companion object {
        private const val TAG = "MainActivity"
        private const val NOTIFICATION_ID = 1
        private const val NOTIFICATION_CHANNEL = "1"
        const val SHOW_STATUS_BAR_ICON = "SHOW_STATUS_BAR_ICON"
        const val HIDE_STATUS_BAR_ICON = "HIDE_STATUS_BAR_ICON"
    }

    private lateinit var mainViewModel: MainViewModel
    private lateinit var mNotifyMgr: NotificationManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(activity_main)
        setSupportActionBar(toolbar)

        mainViewModel = ViewModelProviders.of(this, 
            ViewModelProvider.AndroidViewModelFactory.getInstance(
                application))[MainViewModel::class.java]

        mainViewModel.getProgressLiveData().observe(this, Observer {
            updateUIFromModel()
        })

        mainViewModel.getViewStateLiveData().observe(this, Observer {
            updateUIFromModel()
        })
        ...
    }
}

The MainViewModel is declared, then instantiated by calling AndroidViewModelFactory.getInstance() so that it can then be set up to observe the changes as shown. There's a "single source of truth" concept here, which means the only UI changes take place in the updateUIFromModel() method. This drastically simplifies testing and debugging, because otherwise it would be harder to track down where UI changes came from. Of course this relates to the app's UI itself - the status bar icon is considered separately as a required system feature.

The mechanism to communicate the status bar icon being show or hidden is by using a local BroadcastReceiver - that way it doesn't even need anything special in the manifest. The reason this has to be used for this role is because these messages always get through, unlike, as explained, the ViewModel observables which aren't sent when the app is in the background.

The rest of the app is just your regular layout, button click handling and a multi-state image resource for the pulsating status bar icon.

Overall, this new JetPack MVVM way clearly eliminates many of the headaches which have plagued Android developers for years, whilst promoting efficient design with a solid easy to use testing platform. Use it to create powerful, performant, smaller, maintainable, easy to understand and testable apps henceforth. Oh, and its fun!