Flarosa
Flarosa

Reputation: 1861

How does a Composable function work in Jetpack Compose?

I'm having a hard time understanding Composable functions.

The example provided by Google looks like this:

@Composable
fun MessageCard(name: String) {
    Text(text = "Hello $name!")
}

This is a function that returns no value. The first line of the function instantiates an object of type Text, but assigns it to nothing. I can't see how this can have any effect at all, yet somehow it creates a Text area that ends up on the screen. How?

The example also shows a function being declared outside of a class, which is not something I thought you could do. At least, you can't do it in Java and although I am no expert in Kotlin, I always assumed that it ran on the same JVM and followed the same rules.

Frank

Upvotes: 1

Views: 1210

Answers (2)

Thracian
Thracian

Reputation: 67179

@Composable fun MessageCard(name: String { 
     Text(text = "Hello $name!") 
}

For every default ui Composable like the one above Modifiers and Layout composable are used in implementation.

Layout Composable is basis of every ui composable which is

@Composable
@UiComposable
inline fun Layout(
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val compositeKeyHash = currentCompositeKeyHash
    val materialized = currentComposer.materialize(modifier)
    val localMap = currentComposer.currentCompositionLocalMap
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, SetMeasurePolicy)
            set(localMap, SetResolvedCompositionLocals)
            set(materialized, SetModifier)
            @OptIn(ExperimentalComposeUiApi::class)
            set(compositeKeyHash, SetCompositeKeyHash)
        },
    )
}

It creates a node which by adding containing other children or by being inside other Composables creates a ui node tree.

https://developer.android.com/jetpack/compose/phases

Compose has three main phases:

  1. Composition: What UI to show. Compose runs composable functions and creates a description of your UI.
  2. Layout: Where to place UI. This phase consists of two steps: measurement and placement. Layout elements measure and place themselves and any child elements in 2D coordinates, for each node in the layout tree.
  3. Drawing: How it renders. UI elements draw into a Canvas, usually a device screen.

In first phase a tree is created to show where each ui @Composable should be in this tree. Contents of a composable or other one is set such as for instance for some Composable

CustomLayout() {
    println("Parent Scope")
    CustomLayout() {
        println("Child1 Outer Scope")
        Text("Child1 Outer Content $text")
        CustomLayout() {
            println("Child1 Inner Scope")
            Text("Child1 Inner Content $text")
        }
    }

    CustomLayout() {
        println("Child2 Scope")
        Text("Child2 Content $text")
    }
}

        Parent Layout
         /         \
        /           \
       /             \
      /               \
Child1 Outer         Child2
      |
  Child1 Inner

In second phase with Layout these Layout functions measure their content based on their Modifiers or Constraints, then get total dimensions of its children and also based on its Modifier or Constraints it sets its dimensions. Then places children according to to logic in MeasurePolicy.

https://developer.android.com/jetpack/compose/layouts/custom

Laying out each node in the UI tree is a three step process. Each node must:

Measure any children

Decide its own size

Place its children

After measurement and placement based on how their draw modifier dictates them to draw their content they draw the Composable.

A draw modifier is basically something like this

private class DrawWithContentModifier(
    var onDraw: ContentDrawScope.() -> Unit
) : Modifier.Node(), DrawModifierNode {

    override fun ContentDrawScope.draw() {
        onDraw()
    }
}

And the reason @Composable functions return Unit is to create scope to limit recomposition bounds.

https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78

For every non-inline composable function that returns Unit, the Compose compiler generates code that wraps the function’s body in a recompose scope.

Recompose scopes are an important piece of the Compose puzzle. They do some bookkeeping and help reduce the amount of work Compose has to do to prepare a frame.

They are the smallest unit of a composition that can be re-executed (recomposed) to update the underlying tree. They keep track of what snapshot-based State objects are read inside of them, and get invalidated when those states change.

Upvotes: 3

Jan B&#237;na
Jan B&#237;na

Reputation: 7248

Yes, in Kotlin, you can declare a function outside of class - it is called a top level function. You can't do that in java, so what Kotlin does is they create that class for you and make that top level function a static function of that artificial class. So let's say you have a file Utils.kt:

fun foo(): Int = 1

it is equivalent to this java code:

public class UtilsKt {
    public static int foo() {
        return 1;
    }
}

and UtilsKt.foo() is also how you can call your Kotlin function from Java. You can read more about this in this blogpost.


The answer to how function that seemingly does nothing can actually render UI to the screen is a compiler plugin. Jetpack compose has a Kotlin compiler plugin that takes all Kotlin functions annotated with @Composable and adds code for compose runtime that does the magic. You can read more about it and see how the transformed functions look like here.

Upvotes: 0

Related Questions