January 7, 2018 · Android databinding architecture mvvm

Binding models from LiveData to Android views

tl;dr: You still can't, but keep watching this Github issue. I started binding models straight to view layouts. It works, but read on for better solutions.


For a recent sample project, I created two view controllers (activities) with very similar layouts. I wanted to reuse the layout in both contexts, so I first considered binding a ViewModel instance to the view, and writing the view to read data from the view model. The view model data would be fetched asynchronously, with the requirement to update a RecyclerView adapter when the data object changed: a perfect application for android.architecture.lifecycle.LiveData.

As of December 2017, Android databinding did not support access to LiveData fields, so it wouldn’t work to bind the ViewModel to the View with something like binding.viewModel = MyViewModel() and then access the view model’s LiveData.

It looks like the Android Studio version 31 Canary 6 update experimentally permits databinding to types wrapped by LiveData. But I had two other constraints, too. Because the code would be shared, I couldn’t expect all potential collaborators to upgrade their Android Studio version. And, while they look similar, the two views behave in dramatically different ways. One has lots of logic, while the other is just a portrayal of the model contents. So, I don’t want the same view model type for both.

How can I bind data to a view, if it might come from either a LiveData source or from a plain data model object?

Binding data directly to the view

For this sample app, I bound the data model directly to the view binding. The view template included a RecyclerView, and the more complex view included a requirement to update it. From the activity, I could access the RecyclerView and set its adapter directly, then observe the view model’s data object and make two assignments when the data changed:

  1. update the model object on the binding
  2. update the RecyclerView.Adapter
override fun onCreate(savedInstanceState: Bundle?) {  
    super.onCreate(savedInstanceState)

    binding = DataBindingUtil.setContentView(this, R.layout.activity_person)
    (application as PeopleApplication).applicationComponent.inject(this)

    val viewModelFactory: PersonViewModel.Factory = PersonViewModel.Factory(
    personDataManager, this)
    viewModel = ViewModelProviders.of(this, viewModelFactory).get(PersonViewModel::class.java)

    binding.friendsList.adapter = PersonFriendListAdapter(viewModel)
    viewModel.personData.observe(this, Observer<Person> { person ->
        binding.person = person
        person?.friends?.let {
            (binding.friendsList.adapter as PersonFriendListAdapter).setItems(person.friends)
        }
    })
}

The other use case for the activity_person.xml template was much simpler, and most of that view logic was not necessary for the second controller. So in that activity, I assigned a Person instance to binding.person.

Caveat hacker

Having tried this, I wouldn't do it again. View model logic began to leak outside of the ViewModel. I added a display-logic variable called showFriends to the <data> block in the XML layout. Since the view model didn’t know about it, it could be assigned either in the activity scope or within the View. That was a quick fix for the late hour, but I regretted it. Adding display-controlling variables outside of the scope of a view model defeats the view model’s purpose.

Binding to an ObservableField<T>

Short of relying on the newest databinding features in Android Studio, I would prefer solution #5 found on Christophe Beyls’s blog post, Architecture Components pitfalls — Part 1. Scroll down to “Using Data Binding."

Note to the reader: I’m sorry for linking to a post that uses that meme with the two women and one dude. It's appeared on a few Kotlin blog posts, and it makes me cringe. It’s a shame to see great ideas headlined with an illustration about ignoring one woman and objectifying another. Men of Android development, would you please stop using it? 👌 💯

Beyls’s idea reminds me of a backing field in Kotlin. It uses a LiveData<T> object to hold the lifecycle-controlled data reference, and an ObservableField<T> backed by the LiveData’s value. You write your own observer to keep the ObservableField updated in step with the LiveData. Sure, the data is duplicated, but the data binding code generator can access and use the ObservableField.

His suggestion of a startObserving view model method takes the LifecycleOwner as a parameter. This works around getting that important warning about leaking lifecycle state that you would receive if you were to pass the LifecycleOwner in as a constructor parameter to the ViewModel. That just means that operations that might have affected the value of the LiveData need to be started in startObserving rather than the ViewModel constructor. And having to remember to call startObserving is finicky, but at least it’s a solution to not being able to bind LiveData objects.

P.S. Don’t forget to use a ViewModel.Factory

My other conclusion from this project is that one should not neglect creating a ViewModel that provides a ViewModel.Factory for itself. ViewModel instances that weren't constructed with respect to the lifecycle owner didn't keep their data through an orientation change. The ViewModel constructed from its Factory can save and restore its state data to handle an orientation change.

In hindsight, separating the view model from the view solely to be able to reuse the template was a premature optimization. Creating separate view models for the two different activity controllers would have been tedious, but then I wouldn't have had these lessons to share.

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket
Comments powered by Disqus