alpaca0984.log

Jetpack Compose — Where is recomposed?

alpaca0984

After I published my last post Jetpack Compose - One-time request, I came up with a question if it works in any condition. SharedFlow doesn't persist value inside of it so I expected it would work as a one-time request from ViewModel to Composable Views. However, I wondered if the collected flow would be consumed whenever recomposition is triggered.

I prepared a sample code. If the update block of the AndroidView is evaluated every time I hit the Button, the SharedFlow won't work as a one-time request.

@SuppressLint("SetJavaScriptEnabled")
@Composable
fun EmbeddedWebViewScreen() {
    var count by remember { mutableStateOf(0) }
    var url by remember { mutableStateOf("") }
 
    Column(modifier = Modifier.padding(24.dp)) {
        Button(onClick = { count += 1 }) {
            Text("Count: $count")
        }
 
        Spacer(Modifier.height(16.dp))
        TextButton(onClick = { url = "https://youtube.com" }) {
            Text("YouTube")
        }
 
        Spacer(Modifier.height(16.dp))
        AndroidView(
            factory = { context ->
                WebView(context).apply {
                    webViewClient = WebViewClient()
                    settings.javaScriptEnabled = true
                }
            },
            update = { webView ->
                if (url.isNotEmpty())
                    webView.loadUrl(url)
            }
        )
    }
}

And the result is that hitting the Button and updating count variable doesn't affect the update block. It's evaluated only when the url variable is updated. So, as a conclusion, we can use SharedFlow as one-time requests because it is consumed by Views only when ViewModel emits a new value 🎉

Let's look into the AndroidView() deeply. This is how it looks.

@Composable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
) {
    // bunch of code...
 
    ComposeNode<LayoutNode, UiApplier>(
        factory = {
            // many code...
        },
        update = {
            // ...
            set(update) { viewFactoryHolderRef.value!!.updateBlock = it }
            // ...
        }
    )
 
    // ...
}

The update block is passed to set() function which is impleted as:

/**
 * Set the value property of the emitted node.
 *
 * Schedules [block] to be run when the node is first created or when [value] is different
 * than the previous composition.
 *
 * @see update
 */
fun <V> set(
    value: V,
    block: T.(value: V) -> Unit
) = with(composer) {
    if (inserting || rememberedValue() != value) {
        updateRememberedValue(value)
        composer.apply(value, block)
    }
}

So what it essentially does is that AndroidView remembers (something like taking a snapshot) of the update block. And if it changes during the recomposition, it evaluates the block. And it's not only for AndroidView, of course. Jetpack Compose recomposes UI only where changed.

It reminds me of the Virtual DOM in React. React keeps the DOM structure in memory, and when something changed, it builds the latest DOM structure in memory as well. And with the diffing algorithm, it updates the actual DOM where only needs to be updated. I guess Jetpack Compose does similar things.


After the investigation, I found that it's clearly described in the official doc as:

The Compose framework can intelligently recompose only the components that changed.

Here is the page 😆

When you wonder about something, the first thing you should do is to check out the official doc. But replicating it by yourself and digging into that makes you understand it deeply!

Happy coding!