3

When you declare @Composable Function you are actually declaring a Class

  • Every variable that you declare inside that Function actually acts as Class Property

  • Every Event that you declare inside that Function (like onClick) actually acts as Class Method

This is why variable value is remembered between onClicks. Every click executes Class Method which increases Class Property. This can be seen in Console since we are not updating UI at this point (there is no recomposition taking place).

So the way that Jetpack Compose works and is being explained is totally misleading. It is explained that you work with Functions but in background they behave like Classes in order to make them stateful. So instead of using well known constructs like Classes to implement UI Views they are using Functions with a lot of magic behind the scene to make them behave stateful like Classes. Am I missing something?

//======================================================================
// MAIN ACTIVITY
//======================================================================
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      Column {
        MyComposable("Object1") //Create Object of Class MyComposable
        MyComposable("Object2") //Create Object of Class MyComposable
      }
    }
  }
}

//======================================================================
// MY COMPOSABLE
//======================================================================
// Instead of Function you are actually creating Object behind the scene
// So this Function actually works as Constructor for MyComposable Class    
@Composable
fun MyComposable(name: String) { //CLASS

  //CLASS PROPERTY
  var myProperty = name

  //CLASS METHOD: onClick
  Text(text = myProperty, modifier = Modifier.clickable(onClick = {
    myProperty += " clicked"
    println(myProperty)
  }))

}

Console

Object1 clicked
Object1 clicked clicked

Object2 clicked
ivoronline
  • 829
  • 1
  • 9
  • 18
  • 1
    I haven't dug into how Compose works under the hood, but I'm not sure I understand how you got to that conclusion. Your example just shows that `myProperty` is captured by the `onClick` lambda. – gpunto May 20 '22 at 11:38
  • @gpunto i agree with you. There is nothing in the example above that shows it uses classes under the hood. `Modifier.clickable` is actually a `composed` [Modifier with memory](https://stackoverflow.com/questions/64989659/when-do-you-need-modifier-composed/70169164#70169164) – Thracian May 20 '22 at 11:42
  • "Every variable that you declare inside that Function actually acts as Class Property" -- that is not true. It happens to look that way in this trivial example. Few actual apps are this trivial, and you will discover that your approach will not hold up as you start adding more composables, and compositions start coming and going. "Every Event that you declare inside that Function (like onClick) actually acts as Class Method" -- `onClick` is simply a lambda expression. You are welcome to consider lambda expressions to be "class methods", but not everybody will agree with you. – CommonsWare May 20 '22 at 11:42
  • My point was that how Functions work in Jetpack Compose is not how they work in other languages. Or in other words, how Functions work in Jetpack Compose is more closely related to how Classes work in other languages. We usually think of Functions as being stateles. Once executed their local variables are destroyed. If we want our Function to be stateful, remember state between function calls, then we use Classes which is one reason why classes were created. – ivoronline May 20 '22 at 12:09
  • In other words with functions we don't expect their local variables to survive once the function is executed. While with Classes we do which is what is going on here. We can call it function but it behaves more like Class by having persistent properties, and therefore these Functions act more like Class Constructors. We can call it functions and then create some magic behind the scene that alocates persistent properties to that function - but why not using Classes then instead with whom everybody is familiar and instead inventing hot water in a totally confusing way. – ivoronline May 20 '22 at 12:09
  • "but why not using Classes then instead with whom everybody is familiar and instead inventing hot water in a totally confusing way" -- you are asking for opinions, which is not what this site is for. You might discuss your concerns in Reddit. – CommonsWare May 20 '22 at 12:13
  • I am trying to understand what Jetpack Compose Functions really are since it is obvious that they are not Functions as this term is usually used in programming. That behind the scene they are behaving in a totally different way. So I am trying to find someone who can explain what Jetpack Compose Functions are by using regular terms from programming. If they are some kind of strange hybrid between Classes and Functons which functionalities they take from one and which from the other. Becuase new stuff is most easily explained by comparing it with existing stuff. – ivoronline May 20 '22 at 12:24
  • "In other words with functions we don't expect their local variables to survive once the function is executed." Note that this is not always true. You can return a lambda that captures a local function variable which will then survive the function's scope. – gpunto May 20 '22 at 12:35
  • have you checked you this book about Jetpack Compose Internals? https://jorgecastillo.dev/book/ – Logain May 22 '22 at 19:22

1 Answers1

3

From the book Jetpack Compose Internals

Any function that is annotated as @Composable gets translated by the Jetpack Compose compiler to a function that implicitly gets passed an instance of a Composer context as a parameter, and that also forwards that instance to its Composable children. We could think of it as an injected implicit parameter that the runtime and the developer can remain agnostic of. It looks something like this. Let’s say we have the following composable to represent a nameplate:

NamePlate.kt

@Composable
fun NamePlate(name:String,lastname:String){ 
    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = name)
        Text(text = lastname, style = MaterialTheme.typography.subtitle1)
    }
}

The compiler will add an implicit Composable parameter to each Composable call on the tree, plus some markers to the start and end of each composable. Note that the following code is simplified, but the result will be something like this:

fun NamePlate(name:String,lastname:String, $composer:Composer<*>){
    $composer.start(123)
    Column(modifier = Modifier.padding(16.dp), $composer) {
        Text(text = name, $composer            
        Text(text = lastname, style = MaterialTheme.typography.subtitle1, $composer)
        $composer.end()
    }
}

In the example you are presenting you are basically using a closure that keeps local state by referencing its outer context.

You don't need Compose to achieve this, vanilla Kotlin (or Java for that matter), behaves like this:

fun main() {
    val createCounter = {
        var x = 0
        {
            x += 1
            "x=$x"
        }
    }
    val nextValue = createCounter()

    print(nextValue()) // x=1
    print(nextValue()) // x=2
    print(nextValue()) // x=3
}

While the language allows you to do that, you are actually breaking one of the prerequisites for Compose which is that your functions should be Pure, or at least not have uncontrolled side effects.

What the Compose functions do is similar to a visitor pattern, where the composer is passed down the tree and each function emits to that composer. By being free of sides effects, the compose compiler can assume that if the value of the parameters do not change, then the what the function emits won't change, and so a recomposition (rerun of the function) is not required.

Logain
  • 4,259
  • 1
  • 23
  • 32