ViewModel Under the Hood: How it Works in Compose and View

What happens under the hood when we call viewModel() in Compose or View? How does ViewModelStoreOwner lookup work, what does LocalViewModelStoreOwner do, how does Compose connect to the View hierarchy, and how are DI delegates like hiltViewModel() and koinViewModel() structured? A deep technical look at the scoping and state mechanism.

18min readAndroid
Share:

Introduction

This is a continuation of two previous articles. If in the first one we figured out where ViewModelStore is ultimately stored in the case of Activity, and in the second one — how this is organized in Fragment, today we’ll understand where ViewModels are stored when we use Compose (or even just View).
Especially when we declare ViewModel directly inside Composable functions. But, as always, let’s start with the basics.

There’s such an approach — View-based ViewModel scoping. What does it mean?
We all know the standard practice when each fragment or activity has its own ViewModel.
But there’s also a less popular story — when each View can have its own ViewModel.
How useful this is — it’s up to you to decide. You might ask: what does this have to do with Compose?
And I’ll answer: the thing is, Compose works roughly according to the same scheme. Let’s start with a simple example:

View-based ViewModel scoping — first look

Let’s create a custom View. Let it be TranslatableTextView.
For our example, it’s not so important what exactly this view does — the main thing is that we want to consider the View-based ViewModel scoping approach. Here’s what it might look like:

class TranslatableTextView(context: Context) : AppCompatTextView(context) {

    private val viewModel: TranslatableTextViewViewModel by lazy {
        val owner = findViewTreeViewModelStoreOwner() ?: error("ViewModelStoreOwner not found for TranslatableTextView")
        ViewModelProvider.create(owner = owner).get(TranslatableTextViewViewModel::class.java)
    }

    fun translateTo(locale: Locale) {
        text = viewModel.getTranslatedText(text.toString(), locale)
    }
}

Let’s imagine that TranslatableTextView can translate text, like in Telegram, for example.
If we used a regular ViewModel, we would have to duplicate logic on all screens where this View is used. But thanks to the View-based ViewModel scoping approach, TranslatableTextView has its own ViewModel.

What do we see here?
– Initialization of viewModel directly through ViewModelProvider without delegates, with passing ViewModelStoreOwner.
– A simple translateTo method that takes Locale and updates the view’s text (AppCompatTextView) to the translated one.

Let’s also look at the ViewModel itself to make the example complete and illustrative:

class TranslatableTextViewViewModel : ViewModel() {
    fun getTranslatedText(currentText: String, locale: Locale): String {
        // Here could be real localization
        return "Translated('$currentText') to ${locale.displayLanguage}"
    }
}

Now let’s return to TranslatableTextView to look at the ViewModel initialization in more detail. It looks a bit unusual:

class TranslatableTextView(context: Context) : AppCompatTextView(context) {

    private val viewModel: TranslatableTextViewViewModel by lazy {
        val owner = findViewTreeViewModelStoreOwner() ?: error("ViewModelStoreOwner not found for TranslatableTextView")
        ViewModelProvider.create(owner = owner).get(TranslatableTextViewViewModel::class.java)
    }
    ...
}

The first thing that catches the eye is the call to the findViewTreeViewModelStoreOwner() method.
It returns us a ViewModelStoreOwner, and as we remember, only ComponentActivity, Fragment, or NavBackStackEntry can be one.

Then we pass this owner to ViewModelProvider so that it creates (or returns) the needed ViewModel and places it in the ViewModelStore.
Let me remind you: ViewModelStore is the place where our ViewModel lives and is stored, and it’s available to every ViewModelStoreOwner.

Let’s take a look at how the findViewTreeViewModelStoreOwner() method itself is structured and how it manages to extract the ViewModelStoreOwner:

ViewTreeViewModelStoreOwner.android.kt:

/**
 * Retrieve the [ViewModelStoreOwner] associated with the given [View]. This may be used to retain
 * state associated with this view across configuration changes.
 *
 * @return The [ViewModelStoreOwner] associated with this view and/or some subset of its ancestors
 */
@JvmName("get")
public fun View.findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? {
    var currentView: View? = this
    while (currentView != null) {
        val storeOwner =
            currentView.getTag(R.id.view_tree_view_model_store_owner) as? ViewModelStoreOwner
        if (storeOwner != null) {
            return storeOwner
        }
        currentView = currentView.getParentOrViewTreeDisjointParent() as? View
    }
    return null
}

In short, the following happens in this method: on the current View on which we called findViewTreeViewModelStoreOwner,
we look for a tag with id R.id.view_tree_view_model_store_owner. We cast the obtained value to ViewModelStoreOwner,
and if it’s not null — we return it. And if it’s null, then we start going up the View hierarchy.
This work is performed by the getParentOrViewTreeDisjointParent method. We won’t dive into its sources — it just returns the parent of the current View (direct parent or indirect parent).
Since this happens inside a loop, we go up the hierarchy until we find one of the parents that has the tag R.id.view_tree_view_model_store_owner and already contains a ViewModelStoreOwner.

At this point, in Christopher Nolan style, we temporarily forget about this method — and look at how we’ll use TranslatableTextView:

class MainActivity : AppCompatActivity() {

    private val frameRootLayout by lazy { findViewById<FrameLayout>(R.id.frameRootLayout) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Bind ViewModelStoreOwner to the View tree (frameRootLayout)
        frameRootLayout.setViewTreeViewModelStoreOwner(this)

        val translatableView = TranslatableTextView(this)
        translatableView.text = "Hello, world!"
        frameRootLayout.addView(translatableView)

        // Example of using translation
        translatableView.translateTo(Locale.ENGLISH)
    }
}

Pretty simple, right?
We have some layout where the root is a FrameLayout with id R.id.frameRootLayout.
We find this FrameLayout and add our custom View: TranslatableTextView to it. Everything is clear here.

But the most interesting thing is this line:

// Bind ViewModelStoreOwner to the View tree (frameRootLayout)
frameRootLayout.setViewTreeViewModelStoreOwner(this)

We call setViewTreeViewModelStoreOwner and pass this to it — that is, the Activity itself.
As we know, Activity implements the ViewModelStoreOwner interface,
so we can safely pass it where ViewModelStoreOwner is required.

Here’s what the inheritance chain looks like starting from the ViewModelStoreOwner interface:

[interface] ViewModelStoreOwner → ComponentActivity → FragmentActivity → AppCompatActivity

That is, when we pass this from Activity to setViewTreeViewModelStoreOwner, we pass a completely valid ViewModelStoreOwner, and everything works as expected.
But how exactly does this binding happen inside? What makes findViewTreeViewModelStoreOwner() find this owner (ViewModelStoreOwner) later?

To understand this, let’s look at the source code of the setViewTreeViewModelStoreOwner method, which we’ve already encountered. ViewTreeViewModelStoreOwner.android.kt:


/**
 * Set the [ViewModelStoreOwner] associated with the given [View]. Calls to [get] from this view or
 * descendants will return `viewModelStoreOwner`.
 *
 * This should only be called by constructs such as activities or fragments that manage a view tree
 * and retain state through a [ViewModelStoreOwner]. Callers should only set a [ViewModelStoreOwner]
 * that will be *stable.* The associated [ViewModelStore] should be cleared if the view tree is
 * removed and is not guaranteed to later become reattached to a window.
 *
 * @param viewModelStoreOwner ViewModelStoreOwner associated with the given view
 */
@JvmName("set")
public fun View.setViewTreeViewModelStoreOwner(viewModelStoreOwner: ViewModelStoreOwner?) {
    setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner)
}

The findViewTreeViewModelStoreOwner method, which we’re already familiar with, is also nearby.
Right now we’re interested in setViewTreeViewModelStoreOwner. As we can see, it simply puts the viewModelStoreOwner as a tag in the specified View with the key R.id.view_tree_view_model_store_owner:

setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner)

Everyone who has worked with View knows the setTag(Object?) method, but besides this, there’s also an overloaded method:

public void setTag(int key, final Object tag) {
    ...
}

This method allows storing different tags by keys, using SparseArray under the hood. This is an important point, because it’s exactly through this mechanism that we’ll pass the ViewModelStoreOwner.

Now let’s understand what happens in practice.

In the onCreate method in Activity, we call the setViewTreeViewModelStoreOwner method for the root View (R.id.frameRootLayout), passing this as a parameter, that is, the Activity itself. This is because Activity implements the ViewModelStoreOwner interface. We bind this activity to the view tree to have access to ViewModelStore (since Activity is a ViewModelStoreOwner).

Next, we add our custom View (TranslatableTextView) to this frameRootLayout. Example:

class MainActivity : AppCompatActivity() {

    private val frameRootLayout by lazy { findViewById<FrameLayout>(R.id.frameRootLayout) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Bind ViewModelStoreOwner to the View tree
        frameRootLayout.setViewTreeViewModelStoreOwner(this)

        val translatableView = TranslatableTextView(this)
        translatableView.text = "Hello, world!"
        frameRootLayout.addView(translatableView)

        // Example of using translation
        translatableView.translateTo(Locale.ENGLISH)
    }
}

Now, what happens next?

When we’re in our custom View, we call the findViewTreeViewModelStoreOwner method. This method starts looking for a tag with ID R.id.view_tree_view_model_store_owner in the view itself. If it doesn’t find the needed tag, it will go up the view hierarchy until it finds a parent element that contains this tag:

class TranslatableTextView(context: Context) : AppCompatTextView(context) {

    private val viewModel: TranslatableTextViewViewModel by lazy {
        val owner = findViewTreeViewModelStoreOwner() ?: error("ViewTreeViewModelStoreOwner not found for TranslatableTextView")
        ViewModelProvider.create(owner = owner).get(TranslatableTextViewViewModel::class.java)
    }
    ...
}

So, this mechanism allows finding the needed ViewModelStoreOwner in the view tree, starting from the current view and moving up the hierarchy to the parent component that stores the ViewModelStore.

In our case, findViewTreeViewModelStoreOwner finds the ViewModelStoreOwner in the parent view: FrameLayout(R.id.frameRootLayout), and we get the ViewModelStoreOwner and by default create a ViewModel by calling ViewModelProvider. Ultimately, this way our ViewModel, which we created inside TranslatableTextView, will be stored in the ViewModelStore belonging to the Activity.

Now the question is, why did we consider this? And what does this have to do with Compose? The answer is in the next chapter of the article.

Where does Compose store ViewModels?

Let’s take a very simple ViewModel and a very simple composable screen. Let’s start with the ViewModel:

class MyViewModel : ViewModel() {
    fun getName(): String = "Compose"
}

Our ViewModel is very simple, and we need it only as an example to get to the essence. Next, our Composable Screen:

@Composable
fun Greeting(modifier: Modifier = Modifier) {
    val viewModel = androidx.lifecycle.viewmodel.compose.viewModel<MyViewModel>()
    Text(
        text = "Hello ${viewModel.getName()}",
        modifier = modifier
    )
}

Now let’s continue:

viewModel() is a function from the library: androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7. I specifically indicated the full path to the function in the example so that you’re not confused about where it’s stored and where it came from. Using Koin, for example, we could use koinViewModel() from the io.insert-koin:koin-androidx-compose library, or even hiltViewModel() from androidx.hilt:hilt-navigation-compose.

Regardless of which method we would use to get ViewModel in Compose, they all work under the hood the same way, especially in the context of getting ViewModelStore, since it can’t be taken from thin air. So let’s start studying with androidx.lifecycle.viewmodel.compose.viewModel(), because it was the first, and libraries like Hilt and Koin for creating ViewModel in Compose use a similar mechanism.

Next, the source code of the androidx.lifecycle.viewmodel.compose.viewModel method in the file:

androidx.lifecycle.viewmodel.compose.ViewModel.kt:

@Suppress("MissingJvmstatic")
@Composable
public inline fun <reified VM : ViewModel> viewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    ...
): VM = viewModel(VM::class, viewModelStoreOwner, key, factory, extras)

The other input parameters don’t interest us in this article, except for the viewModelStoreOwner parameter:

viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
    "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},

Next, we’ll be interested in LocalViewModelStoreOwner.current - since it provides us with ViewModelStore, apparently. LocalViewModelStoreOwner.current from the name and syntax, it’s immediately clear that this is CompositionLocal:

CompositionLocal is a mechanism in Jetpack Compose that allows passing values through the UI tree without explicit passing through parameters, with access to them via .current at any point in the composition. For use, it’s necessary to previously provide a value through CompositionLocalProvider or set it by default when creating.

Let’s look at the LocalViewModelStoreOwner source code:

/**
 * The CompositionLocal containing the current [ViewModelStoreOwner].
 */
public object LocalViewModelStoreOwner {
    private val LocalViewModelStoreOwner =
        compositionLocalOf<ViewModelStoreOwner?> { null }

    /**
     * Returns current composition local value for the owner or `null` if one has not
     * been provided nor is one available via [findViewTreeViewModelStoreOwner] on the
     * current [androidx.compose.ui.platform.LocalView].
     */
    public val current: ViewModelStoreOwner?
        @Composable
        get() = LocalViewModelStoreOwner.current ?: findViewTreeViewModelStoreOwner()

    /**
     * Associates a [LocalViewModelStoreOwner] key to a value in a call to
     * [CompositionLocalProvider].
     */
    public infix fun provides(viewModelStoreOwner: ViewModelStoreOwner):
            ProvidedValue<ViewModelStoreOwner?> {
        return LocalViewModelStoreOwner.provides(viewModelStoreOwner)
    }
}

We see that LocalViewModelStoreOwner is just a wrapper over the real CompositionLocal. We access exactly its current field to read the current value. We either try to get the value from the current field of CompositionLocal — this means that someone somewhere should have provided it. If it’s empty there, then in that case the findViewTreeViewModelStoreOwner method is called. In the usual out-of-the-box usage scenario, we fall exactly under the second case when the findViewTreeViewModelStoreOwner method is called. So let’s look at its source code next:

LocalViewModelStoreOwner.android.kt

@Composable
internal actual fun findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? =
    LocalView.current.findViewTreeViewModelStoreOwner()

And we see that another CompositionLocalLocalView calls the View.findViewTreeViewModelStoreOwner() method — this is the same method we already looked at in the first part of the article. LocalView.current returns us the current View. Current View? Aren’t we working in compose now? Where did the current View come from? We’ll find out about this a bit later, what this View is and where it came from. For now, just know that under the hood LocalView.current returns us the current View, on which we can call the extension function findViewTreeViewModelStoreOwner, which we already saw in the first part of the article, and will put the ViewModel in ViewModelStore:

ViewTreeLifecycleOwner.android.kt

/**
 * Retrieve the [ViewModelStoreOwner] associated with the given [View]. This may be used to retain
 * state associated with this view across configuration changes.
 *
 * @return The [ViewModelStoreOwner] associated with this view and/or some subset of its ancestors
 */
@JvmName("get")
public fun View.findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? {
    var currentView: View? = this
    while (currentView != null) {
        val storeOwner =
            currentView.getTag(R.id.view_tree_view_model_store_owner) as? ViewModelStoreOwner
        if (storeOwner != null) {
            return storeOwner
        }
        currentView = currentView.getParentOrViewTreeDisjointParent() as? View
    }
    return null
}

Let’s go through the flow once more:

When we call any of the extension functions for creating viewmodel inside our Composable functions: whether it’s viewModel from the androidx.lifecycle:lifecycle-viewmodel-compose library, or even koinViewModel() from the io.insert-koin:koin-androidx-compose library, or even hiltViewModel() from androidx.hilt:hilt-navigation-compose, we ultimately access exactly the CompositionLocal named LocalViewModelStoreOwner to its current field. And that, in turn, either gets the value stored inside it, or calls the Composable method findViewTreeViewModelStoreOwner. And that, in turn, calls LocalView — this is another CompositionLocal that has the current View, and for it the extension method View.findViewTreeViewModelStoreOwner is launched, and a search through the View tree occurs looking for ViewModelStoreOwner. In the end, it finds it, but how? Two questions arise in mind:

  1. What do Views have to do with this? Why does Compose call LocalView, and where did LocalView itself come from?
  2. From the previous chapter in the article, we saw that before calling the View.findViewTreeViewModelStoreOwner() method, we put ViewModelStoreOwner into an internal tag inside FrameLayout, which was the root View in our layout, using the setViewTreeViewModelStoreOwner method. But in the Compose example, we didn’t put anything anywhere — how does all this work by itself?

Everything is quite simple, Google developers took care of this for us. Usually in Composable there are two approaches:

  1. When the entire project is fully on Compose, or at least in each activity the UI tree starts with setContent{}, not with setContentView:

    class MainActivity : ComponentActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                Greeting(modifier = Modifier.fillMaxWidth())
            }
        }
    }
  2. Hybrid UI, where part is on compose, and part is on View. Then they resort to using ComposeView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/linearLayout"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <androidx.compose.ui.platform.ComposeView
            android:id="@+id/composeView"
            android:layout_width="match_parent"
            android:layout_height="200dp"/>
</LinearLayout>
class MainActivity : ComponentActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val composeView = findViewById<ComposeView>(R.id.composeView)

        composeView.setContent { Greeting() }
    }
}

In both cases, if you run it as it is now, everything will work: our ViewModel inside the Greeting function will be created without problems and placed in the ViewModelStore, which belongs to the Activity. Why does this happen?

In both cases, we call the setContent method, in the first case it’s ComponentActivity.setContent{}, and in the second ComposeView.setContent {}, which open the Composable area.

Let’s first consider the first case, starting with setContent for activity (ComponentActivity).

Using ComponentActivity.setContent:

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    val existingComposeView =
        window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as? ComposeView

    if (existingComposeView != null)
        with(existingComposeView) {
            setParentCompositionContext(parent)
            setContent(content)
        }
    else
        ComposeView(this).apply {
            // Set content and parent **before** setContentView
            // to have ComposeView create the composition on attach
            setParentCompositionContext(parent)
            setContent(content)
            // Set the view tree owners before setting the content view so that the inflation
            // process and attach listeners will see them already present
            setOwners()
            setContentView(this, DefaultActivityContentLayoutParams)
        }
}

Note that the setContent function is an extension for ComponentActivity and has additional logic for initializing Owners and other components. Inside itself, it uses ComposeView and its setContent method.

What’s happening here? The window has a DecorView, inside this DecorView there’s another ViewGroup (FrameLayout). From this ViewGroup, a ComposeView is extracted at index 0, if it exists. If it doesn’t exist, then a new one is created and the setContentView method is called (which all activities have and is inherited from Activity itself). But what we need happens before calling the setContentView method — we’re talking about setOwners. Let’s look at its source code too:

private fun ComponentActivity.setOwners() {
    val decorView = window.decorView
    ...
    if (decorView.findViewTreeViewModelStoreOwner() == null) {
        decorView.setViewTreeViewModelStoreOwner(this)
    }
    ...
}

And this is exactly where ViewModelStoreOwner is placed in DecorView by calling the setViewTreeViewModelStoreOwner method, where this is passed — that is, the activity itself. DecorView is the most (almost) root View in the entire View hierarchy, above it stands only the Window itself.

Overall Picture of ViewModelStoreOwner, ComposeView and LocalView Interaction

Now let’s summarize the entire process and draw conclusions: when we use ComponentActivity (or its descendants FragmentActivity and AppCompatActivity) in Compose and create a ViewModel using compose/hilt/koin delegates, then inside there’s a call to LocalViewModelStoreOwner. It returns ViewModelStoreOwner, if it exists. If not, then it calls the Composable method findViewTreeViewModelStoreOwner. That, in turn, inside itself calls composition local — LocalView.current, gets View and calls another extension method View.findViewTreeViewModelStoreOwner on this View. This method recursively, starting from LocalView, searches for the saved ViewModelStoreOwner in View tags and thus climbs up the View hierarchy until it finds one. If it finds it, it returns it; if it doesn’t find it, it returns null, and an error is thrown: No ViewModelStoreOwner was provided via LocalViewModelStoreOwner

As we saw above, when calling ComponentActivity.setContent{}, under the hood the method ComponentActivity.setOwners() is called, which places ViewModelStoreOwner in the DecorView tag. It turns out that when calling the View.findViewTreeViewModelStoreOwner() method, climbing through the View hierarchy, ultimately ViewModelStoreOwner is found inside the topmost View (DecorView), but in Compose there’s no direct access to DecorView, instead there’s a call to LocalView.current:

LocalViewModelStoreOwner.android.kt

@Composable
internal actual fun findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? =
    LocalView.current.findViewTreeViewModelStoreOwner()

In this chain we haven’t considered only one moment — where does LocalView come from. More precisely, it’s clear that this is CompositionLocal, but where is the reference to the current View in it? or who is the current View?

If briefly and abstractly: ComposeView inside itself calls LocalView and provides itself to it. Therefore LocalView by default refers to that ComposeView in which the tree of Composable functions was launched. And the Compose tree in Android always starts exactly from ComposeView.

Below — the full path to the moment where LocalView gets its value. Without detailed comments, just the chain:

class ComposeView @JvmOverloads constructor(...) : AbstractComposeView(context, attrs, defStyleAttr)

ComposeView inherits from AbstractComposeView. Let’s see what happens inside AbstractComposeView:

abstract class AbstractComposeView(...) : ViewGroup(...) {
    private fun ensureCompositionCreated() {
        if (composition == null) {
            composition = setContent(resolveParentCompositionContext()) {
                Content()
            }
        }
    }
}

In the ensureCompositionCreated method, which is called, for example, on onMeasure or onAttachedToWindow, or when we call ComposeView.setContent, we’re interested in the call to the setContent function:

internal fun AbstractComposeView.setContent(...): Composition {
    val composeView = ... ?: AndroidComposeView(...).also {
        addView(it.view, DefaultLayoutParams)
    }
    return doSetContent(composeView, parent, content)
}

Here the following happens: an object of the AndroidComposeView class is created, this same object is placed inside ComposeView by calling addView. I remind you that AbstractComposeView is an abstract class, and one of its descendants is ComposeView. Although work here is done at the abstraction level, actually when addView is called, it’s called for ComposeView.

If there are too many new names that cause confusion, here’s a brief explanation:

  • AbstractComposeView - abstract class that is a ViewGroup and already has many implementations inside
  • ComposeView - one of the descendants of AbstractComposeView that allows us to run Composable functions inside itself. In Android everything ultimately comes down to working with it, since in Android there’s no way to run Composable directly at the Window level. Between Window and our Composable screens there are a bunch of View and ViewGroup, including ComposeView itself
  • AndroidComposeView - low-level class, inside which ultimately our Composable screens are drawn

Next — doSetContent:

private fun doSetContent(
    owner: AndroidComposeView,
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    ...
    val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
            as? WrappedComposition
        ?: WrappedComposition(owner, original).also {
            owner.view.setTag(R.id.wrapped_composition_tag, it)
        }
    wrapped.setContent(content)
}

We move to WrappedComposition.setContent:

private class WrappedComposition(
    val owner: AndroidComposeView,
    val original: Composition
) : Composition, LifecycleEventObserver, CompositionServices {
    override fun setContent(content: @Composable () -> Unit) {
        ...
        ProvideAndroidCompositionLocals(owner, content)
        ...
    }
}

And here’s — the key moment:

@Composable
internal fun ProvideAndroidCompositionLocals(
    owner: AndroidComposeView,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        ...
    LocalView provides owner.view,
    ...
    ) {
        content()
    }
}

Here LocalView gets the value owner.view, where owner is AndroidComposeView, created inside ComposeView.

Conclusion: LocalView gets a reference to View, inside which the composition is executed, due to the fact that ComposeView itself initializes AndroidComposeView, which is then passed to ProvideAndroidCompositionLocals. AndroidComposeView is created and stored inside ComposeView, and LocalView refers exactly to this AndroidComposeView, and not to ComposeView itself.

ComposeView inherits from AbstractComposeView, which in turn is ViewGroup. That is, ComposeView is not AndroidComposeView itself, but just a container that creates AndroidComposeView when calling setContent and inserts it inside.

Therefore, when in ProvideAndroidCompositionLocals this happens:

LocalView provides owner.view

owner.view is AndroidComposeView, not ComposeView.

View hierarchy, if Activity is AppCompatActivity, will look like this:

ViewRootImpl
└── DecorView -> has weak reference to ViewModelStoreOwner (i.e. activity)
    └── LinearLayout
        └── FrameLayout
            └── FitWindowsLinearLayout (action_bar_root)
                └── ContentFrameLayout (android:id/content)
                    └── ComposeView
                        └── AndroidComposeView -> has weak reference to ViewModelStoreOwner (i.e. activity)

And if it’s ComponentActivity or FragmentActivity, then slightly shorter:

ViewRootImpl
└── DecorView -> has weak reference to ViewModelStoreOwner (i.e. activity)
    └── LinearLayout
        └── FrameLayout (android:id/content)
            └── ComposeView
                └── AndroidComposeView -> has weak reference to ViewModelStoreOwner (i.e. activity)
Interesting fact

ViewRootImpl is the root element of the entire View hierarchy. In practice, every Android developer has encountered at least once the error:

“Only the original thread that created a view hierarchy can touch its views.”

This error occurs if you try to access View from a non-UI thread. And it’s thrown exactly by ViewRootImpl inside the checkThread() method:

public final class ViewRootImpl implements ViewParent, ... {

    void checkThread() {
        Thread current = Thread.currentThread();
        if (mThread != current) {
            throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views."
                + " Expected: " + mThread.getName()
                + " Calling: " + current.getName());
        }
    }
}

The key idea — LocalView by default points to AndroidComposeView, which is created inside ComposeView dynamically. ComposeView itself is just a wrapper that knows how to connect everything and embed the Composable tree in the right place of the hierarchy.

Here we’ve considered the first case, when we use ComponentActivity.setContent with passing our composition and creating ViewModel. The second usage flow is inside the View hierarchy, for example, if we have all screens on Fragment/View, and we use Compose in some places. This is possible thanks to ComposeView. Let’s consider such a case:

Using ComposeView.setContent:

Here’s an example code from the examples above:

class MainActivity : ComponentActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val composeView = findViewById<ComposeView>(R.id.composeView)

        composeView.setContent { Greeting() }
    }
}
@Composable
fun Greeting(modifier: Modifier = Modifier) {
    val viewModel = androidx.lifecycle.viewmodel.compose.viewModel<MyViewModel>()
    Text(
        text = "Hello ${viewModel.getName()}",
        modifier = modifier
    )
}

We’ve already considered how setContent works in ComposeView. Inside itself ComposeView.setContent doesn’t put a reference to ViewModelStoreOwner and doesn’t have inside itself a call to the setViewTreeViewModelStoreOwner function, it only helps provide LocalView.

But if we run the code in its current form, everything will work as expected. What’s the matter? The situation is similar to before, when such logic was already provided for us. The thing is the following: when calling the setContentView(R.layout.activity_main) method or even when passing a layout reference to the constructor: ComponentActivity(R.layout.activity_main) the following chain occurs:

If we pass Layout Id to the constructor:

open class ComponentActivity() ... {

    @ContentView
    constructor(@LayoutRes contentLayoutId: Int) : this() {
    this.contentLayoutId = contentLayoutId
}

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        if (contentLayoutId != 0) {
            setContentView(contentLayoutId)
        }
    }
}

In the onCreate method, setContentView is called if contentLayoutId was passed to the constructor. If setContentView was called directly, then the logic is as follows:

When we call the setContentView() method and pass our View or layout id, then under the hood the following happens (below are the sources of the setContentView method):

open class ComponentActivity() ... {

    override fun setContentView(@LayoutRes layoutResID: Int) {
        initializeViewTreeOwners()
        reportFullyDrawnExecutor.viewCreated(window.decorView)
        super.setContentView(layoutResID)
    }
}

The name of the initializeViewTreeOwners method looks tempting, so let’s look at the sources:

@CallSuper
open class ComponentActivity() ... {

    open fun initializeViewTreeOwners() {
        ...
        window.decorView.setViewTreeViewModelStoreOwner(this)
        ...
    }
}

And here we see that the window calls the getDecorView method (in Kotlin all getters from Java have syntax like a variable), and then the setViewTreeViewModelStoreOwner function is called, which places this (ViewModelStoreOwner) in the tag inside DecorView.

Let’s draw conclusions: when we start our UI with the setContentView method or pass layout id to the activity constructor, then inside ComponentActivity itself (which is the parent for FragmentActivity and AppCompatActivity) logic triggers that places itself (activity implements the ViewModelStoreOwner interface) into the internal tag of DecorView (which is almost the highest in the hierarchy) by calling the setViewTreeViewModelStoreOwner method. Then, when we add our ComposeView to the View hierarchy to start writing in Compose, then inside ComposeView the value for LocalView.current is provided. Then when creating ViewModel inside Compose there’s a call to LocalViewModelStoreOwner, namely to its current field. There it’s checked if there’s a value, and if not, the findViewTreeViewModelStoreOwner method is called on LocalView, which searches for ViewModelStoreOwner, climbing up the hierarchy until it finds one. Thus, ultimately ViewModelStoreOwner is found in DecorView. That’s how it all works. Below is a diagram of the View hierarchy:

ViewRootImpl
└── DecorView -> has weak reference to ViewModelStoreOwner (i.e. activity)
    └── LinearLayout
        └── FrameLayout (android:id/content)
            └── FrameLayout (app:id/frameRootLayout)
                └── ComposeView (app:id/composeView)
                    └── AndroidComposeView

The article is almost finished, it remains to shed light on one point. By this point all the information above suggests the thought: why did we manually call the setViewTreeViewModelStoreOwner method at the beginning of the article, if all this is done for us?

(P.S. I’m returning to the example at the beginning of the article with View (TranslatableTextView))

Thanks to the fact that we set ViewModelStoreOwner for our root layout inside our layout, the tag inside FrameLayout (frameRootLayout) has a reference (weak) to ViewModelStoreOwner:

class MainActivity : AppCompatActivity() {

    private val frameRootLayout by lazy { findViewById<FrameLayout>(R.id.frameRootLayout) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        // Bind ViewModelStoreOwner to View tree
        frameRootLayout.setViewTreeViewModelStoreOwner(this)
        ...
    }
}

And the findViewTreeViewModelStoreOwner method, when it runs through the View hierarchy, will first look in TranslatableTextView, and then, if it doesn’t find it, will climb up through the parents. The parent is frameRootLayout (FrameLayout), there it will find ViewModelStoreOwner. But what if we remove the setting frameRootLayout.setViewTreeViewModelStoreOwner(this) and run the code?

class TranslatableTextView(context: Context) : AppCompatTextView(context) {

    private val viewModel: TranslatableTextViewViewModel by lazy {
        val owner = findViewTreeViewModelStoreOwner() ?: error("ViewModelStoreOwner not found for TranslatableTextView")
        ViewModelProvider.create(owner = owner).get(TranslatableTextViewViewModel::class.java)
    }
    ...
}

Everything will still work the same. Why? The thing is that, as we’ve already considered in the hierarchy, there’s another parent — DecorView. Here’s how it looks:

ViewRootImpl
└── DecorView -> has weak reference to ViewModelStoreOwner (i.e. activity)
    └── LinearLayout
        └── FrameLayout (android:id/content)
            └── FrameLayout (app:id/frameRootLayout)
                └── TranslatableTextView

And when we call the AppCompatActivity.setContentView() method and pass our View or layout id, then under the hood the following happens (below are the sources of the setContentView method):

open class ComponentActivity() ... {

    override fun setContentView(@LayoutRes layoutResID: Int) {
        initializeViewTreeOwners()
        ...
    }
}

The name of the initializeViewTreeOwners method looks tempting, so let’s look at the sources:

@CallSuper
open class ComponentActivity() ... {

    open fun initializeViewTreeOwners() {
        ...
        window.decorView.setViewTreeViewModelStoreOwner(this)
        ...
    }
}

The conclusion is this: call setViewTreeViewModelStoreOwner only if you want to specify yourself in which View you want to place a specific ViewModelStoreOwner. In Compose call LocalViewModelStoreOwner provides yourViewModelStoreOwner only if you have a need for this, but in practice I haven’t encountered anyone doing this, since out-of-the-box solutions from Google solve everything, and manual work is usually not necessary — unless you’re really doing something very custom.

ViewModel Compose DI Delegates:

When we considered ViewModel for Composable functions, we only considered the composable function viewModel() — a function from the library: androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7 without DI. And the initialization was like this:

@Composable
fun Greeting(modifier: Modifier = Modifier) {
    // here I specifically didn't import the function
    val viewModel = androidx.lifecycle.viewmodel.compose.viewModel<MyViewModel>()
}

Earlier I said that:

When we call any of the extension functions for creating viewModel inside our Composable functions: whether it’s

  1. viewModel from the library androidx.lifecycle:lifecycle-viewmodel-compose,
  2. koinViewModel() from the library io.insert-koin:koin-androidx-compose,
  3. hiltViewModel() from androidx.hilt:hilt-navigation-compose,

Then ultimately we call exactly the CompositionLocal named LocalViewModelStoreOwner to its current field. Therefore, the implementation is the same everywhere regardless of the library, the entire flow we considered regardless of the delegate and library will work the same way.

Let’s make sure of this, just look at the signature of all three:

  1. We’ve already seen the first one, let’s look again: androidx.lifecycle.viewmodel.compose.ViewModel.kt

    @Suppress("MissingJvmstatic")
    @Composable
    public inline fun <reified VM : ViewModel> viewModel(
        viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
            "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
        },
        ...
    ): VM = viewModel(VM::class, viewModelStoreOwner, key, factory, extras)
  2. Koin: org.koin.androidx.compose.ViewModel.kt:

@OptIn(KoinInternalApi::class)
@Composable
inline fun <reified T : ViewModel> koinViewModel(
    qualifier: Qualifier? = null,
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    ...
): T {
    return resolveViewModel(
        T::class, viewModelStoreOwner.viewModelStore, key, extras, qualifier, scope, parameters
    )
}
  1. Hilt: androidx.hilt.navigation.compose.HiltViewModel.kt:
@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null
): VM {
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(viewModelStoreOwner, key, factory = factory)
}

As you can notice, all three delegates — viewModel(), koinViewModel() and hiltViewModel() — use the same mechanism for getting ViewModelStoreOwner through LocalViewModelStoreOwner.current. The differences are only in syntax and additional logic related to DI, but at the core everything comes down to one thing — getting ViewModelStoreOwner from the View tree.

The reason is simple: in Compose there’s no direct access to ComponentActivity and its derivatives (FragmentActivity, AppCompatActivity), as well as to Fragment or NavBackStackEntry. Therefore, LocalViewModelStoreOwner is used, which in the absence of a value in current calls LocalView.current and already for it calls the findViewTreeViewModelStoreOwner() method — the standard way to get the nearest ViewModelStoreOwner from the View hierarchy.

That’s exactly why LocalViewModelStoreOwner is a key element. It’s a universal mediator between Compose and the traditional ViewModel mechanism of Android. And regardless of whether you use Hilt, Koin, or nothing from DI, — everything works through it.

Discussion

Comments