Level-up your custom Views

Nick Rose
5 min readJul 23, 2021

--

I was recently tasked with writing a piece of UI that depended on asynchronous streams of data, had to be usable in a RecyclerView, and had to be exposed by a standalone library. Usually whenever I churn out such a feature, it’d involve some combination of a Fragment, ViewModel, and repository all working together to ultimately set data on one or more framework Views. Having to bundle all that beneath an API surface of a custom View was new to me and initially I wasn’t sure how to proceed.

It turns out that there have been some View APIs hanging around for about a year now that help out with this, and they’ve really upended my perspective of how Android screens can be built (at least, until Compose hits stable 😏). They are three Kotlin extension functions (which forward to Java static methods):

View.findViewTreeLifecycleOwner()
View.findViewTreeViewModelStoreOwner()
View.findViewTreeSavedStateRegistryOwner()

With these three functions, you have all the required tools at your disposal to access and use a ViewModel within a View. This is great because it means work can be done inside that ViewModel, and your custom View can listen to the ViewModel’s emissions and the View can update its own UI accordingly, not unlike how a Fragment would.

How would this look like? I’m glad you asked. Here’s a short example:

class MyCustomView : FrameLayout {
// ... View constructors omitted
private val viewModel by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!)
.get(MyViewModel::class.java)
}
}

There’s no out-of-the-box by viewModels() View extensions like Fragments/Activities have, so without writing you’re own you’ll have to use the explicit way to get a handle on a ViewModel via ViewModelProvider instantiation. Here it’s being used to get a MyViewModel instance, which has an implementation that can be ignored.

The main part of this code snippet is the findViewTreeViewModelStoreOwner() function call. A ViewModelStore is simply a class that owns ViewModels and, most importantly, survives configuration changes through how a ViewModelStoreOwner works. Views already have a state saving mechanism to reconcile configuration changes, but it only works for serializable data; Views aren’t ViewModelStoreOwners. ViewModels on the other hand will entirely persist any ongoing work and complex objects across configuration changes, when attached to a ViewModelStore. On the other, other hand, View state saving persists across process recreation and ViewModels don’t, so you’ll still want to use a combination of both.

The underlying implementation of this findViewTreeViewModelStoreOwner() extension function queries the View tree until it finds a ViewModelStoreOwner. If MyCustomView were created within a Fragment, that function would return a FragmentViewLifecycleOwner which is also a ViewModelStoreOwner. If MyCustomView were created within an Activity, that function would return the Activity itself, which is also a ViewModelStoreOwner.

I want to reiterate that Views are not ViewModelStoreOwners and that Views need to reference another class like a Fragment or Activity (both of which are ViewModelStoreOwners) via findViewTreeViewModelStoreOwner() in order to get a ViewModel. This means that if the ViewModelStoreOwner that MyCustomView queried to create a ViewModel were an Activity, that created ViewModel would be owned by the Activity and live inside the Activity for the entirety of the Activity scope (more specifically, until the last Activity.onDestroy()). So if you have a RecyclerView of these custom Views and they each ask for a MyViewModel, then they will all get the same ViewModel. Think of it like how Fragment.activityViewModels works. You’ll want to build a strategy around making one ViewModel work for all MyCustomViews in that list. Alternatively, each MyCustomView could use the ViewModelProvider.get(String key, Class<T> modelClass) overload to get a unique ViewModel per MyCustomView, but this will create a lot of ViewModels if your RecyclerView has a lot of items! That may or may not matter, depending on how much you care about the potential bloat.

We now know how to get a ViewModel within a View — great, but how will that View subscribe to LiveData/Flow emissions within the ViewModel in a lifecycle conscious way? One of the three functions I mentioned earlier is findViewTreeLifecycleOwner(), but this might not be the right choice for LiveData observing. Ideally we’ll want to use a lifecycle that spans the lifecycle of the View itself, not the one that the LifecycleOwner this extension function finds up the view tree. This is because an individual View can have a shorter lifecycle than the view tree. For example, a View can be removed from a ViewGroup arbitrarily. Any subscriptions to the ViewModel that the View owns will want to be cancelled the moment the View is removed from the view tree. In light of this, defining a custom lifecycle within the View spanning the onAttachedToWindow() and onDetachedFromWindow() View callbacks is the way to go. If you’re only using Flows in your ViewModel, a custom Lifecycle might not be needed (it will be needed if you use Flow.repeatOnLifecycle); instead, scope a CoroutineScope to those same callbacks, like so:

private var scope: CoroutineScope? = null

override fun onAttachedToWindow() {
super.onAttachedToWindow()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope?.cancel()
scope = null
}

The code above makes sure that any ongoing work will be cleaned up when the View is detached from the window. View detachment from the window usually means the View is getting blown away (View reparenting is one exception to this).

What findViewTreeLifecycleOwner() is particularly useful for is allowing your custom View to listen and react to useful view tree lifecycle events, like ON_START and ON_STOP. For example, if your View’s ViewModel was doing work that only mattered while the app was in the foreground, that work could be stopped after the ON_STOP event, and restarted after ON_START.

The last of the three View extension functions is findViewTreeSavedStateRegistryOwner(), which actually comes from the androidx.savedstate artifact (instead of androidx.lifecycle, which the other two are from). This can give a custom View access to a SavedStateRegistry. Even though Views already have a mechanism for state saving, this is useful to have so that the SavedStateRegistry can be passed to other classes that don’t inherit from View/Fragment/Activity and enable them to participate in state saving, for example, ViewModel.

And there you have it. Suddenly Fragments seem even less attractive than they were before, now that Views can get a handle on a ViewModel. Like all choices in Android, there are risks involved with this approach, which I went into before: if you’re not careful then it’s easy to either bloat your ViewModelStore with loads of ViewModels, or the store could return you a stale ViewModel instance when you were expecting a fresh one. For library developers of custom Views, these extension functions create very interesting possibilities because it means not needing to ask clients that depend on your library to manage a lot of complex work in their own code in order to make your custom View work. Instead, your custom View can take ownership of that burden an expose a very lightweight API.

If you’re interested in looking at a simple example of these APIs, I’ve pushed a repository to this link: https://github.com/nihk/view-tree-fun/tree/main

--

--