All Articles

Using ViewModels in custom views

Before we jump into how to use ViewModel’s in custom views, let’s take a look at why you would want to do something like this.

Let’s consider a screen with a list of views showing a bunch of data and having some state. While it’s entirely possible to handle this scenario at your screen level, you would be doing a bunch of things in your state management and it becomes harder to manage and test.

One way to simplify the state management of this screen is to extracting out it into a bunch of custom views that manage their state. (You don’t have to use a ViewModel to do this, but this article focuses on Jetpack ViewModel)

Alright, now that we established some context. Let’s look at code now.

Creating a ViewModel in the view

Great, you have extracted out the view from your screen to a custom view, and you have created a ViewModel to accompany that. Now, all you have to do is use ViewModelProvider to create/get the ViewModel.

Here comes your first issue, to create/get a ViewModel from the ViewModelProvider you would need to provide access to the ViewModelStoreOwner when constructing ViewModelProvider, and View is not a ViewModelStoreOwner.

So, you have 2 ways to get the store owner.

  • Implementing ViewModelStoreOwner interface in your custom view
  • Getting nearest ViewModelStoreOwner

Implementing ViewModelStoreOwner interface in your custom view

I won’t go into too much detail about the first option, but I didn’t prefer this, since it requires us to implement the interface for every custom view where we want to use ViewModel. You can abstract way this with a base class, but you would end up creating a bunch of base classes for different view group types.

Although, this option is good if you want to scope your ViewModel lifecycle to the custom view and clear it once your view is destroyed. But for our use case, we will be placing these custom views inside a screen (fragment or activity) which is a ViewModelStoreOwner, and want our ViewModel’s to be scope to that screen.

Getting nearest ViewModelStoreOwner

In this approach, we would want to get the nearest ViewModelStoreOwner which is usually an Activity, Fragment, or a custom store owner. So, how do we do that?

Let me introduce you to ViewTreeViewModelStoreOwner, this allows us to get the nearest ViewModelStoreOwner for a view. This checks the ancestors of the view to get the store owner. Which is exactly what we need.

Let’s take a look at an example:

You can use ViewTreeViewModelStoreOwner#get to get the nearest ViewModelStoreOwner and provide it when constructing the ViewModelProvider.

private val viewModel by lazy {
    ViewModelProvider(ViewTreeViewModelStoreOwner.get(this)!!).get<SummaryViewModel>()
}

If you’re using viewmodel-ktx artifact, you can use findViewTreeViewModelStoreOwner extension.

private val viewModel by lazy {
    ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SummaryViewModel>()
}

This will create/get the ViewModel scoped to the screen ViewModel lifecycle.

Important: Since ViewTreeViewModelStoreOwner relies on getting store owner from the ancestors, make sure you are calling it in onAttachedToWindow.

Bonus

Now you have your ViewModel’s in your custom views, but how do you observe LiveData or lifecycle-aware observers?

Well, again there are two options

  • Implementing LifecycleOwner interface in your custom view
  • Getting nearest LifecycleOwner using ViewTreeLifecycleOwner

Both of these are good options depending on which lifecycle scope you would want your observer to observe.


Here is the full code for the custom view

class SummaryView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {

  private val viewModel by lazy {
    ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SummaryViewModel>()
  }

  override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    viewModel.summaryModel.observe(findViewTreeLifecycleOwner()!!, ::populateSummaryView)
  }

  private fun populateSummaryView(summaryModel: SummaryModel) {
    // do stuff
  }
}

Well, that’s all folks. Until next time 👋🏾