HelloCW
HelloCW

Reputation: 2355

What are differents between Composable function and normal function in Android?

I have read the official document about Composable functions

But I can't understand Composable functions.
For example, rememberWatchState() is a Composable function in Code A, but it doesn't define my app's UI just like a normal function.

What is the difference between the Composable function and the normal function in Android?

Code A

@Composable
fun ScreenHome_Watch(
    watchState:WatchState =  rememberWatchState()
){
    val density= watchState.density
}


@Composable
fun rememberWatchState(): WatchState {
    val context: Context = LocalContext.current
    val temp = loadDBWarningValueWithPreference(context)

    val watchState = WatchState(temp.toFloat(),LocalDensity.current)

    return remember {
        watchState    
    }
}

class WatchState(
    private val alarmValue:Float,
    val density: Density
){
    ...
}


fun drawDial(
    drawScope: DrawScope,
    watchState:WatchState
) {
   ...
}

Upvotes: 7

Views: 3006

Answers (1)

Thracian
Thracian

Reputation: 67423

@Composable annotation is like a scope that gives access to Compose functions such as LaunchedEffect, SideEffect, remember or objects such as currentComposer and resembles suspend functions.

Section below is quoted from Under the hood of Jetpack Compose — part 2 of 2 article by Leland Richardson.

// function declaration
suspend fun MyFun() { … }
 
// lambda declaration
val myLambda = suspend { … }

// function type
fun MyFun(myParam: suspend () -> Unit) { … }

Kotlin’s suspend keyword operates on function types: you can have a function declaration that’s suspend, a lambda, or a type. Compose works in the same way: it can alter function types.

// function declaration
@Composable fun MyFun() { … }
 
// lambda declaration
val myLambda = @Composable { … }
 
// function type
fun MyFun(myParam: @Composable () -> Unit) { … }

The important point here is that when you annotate a function type with @Composable you’re changing its type: the same function type without the annotation is not compatible with the annotated type. Also, suspend functions require a calling context, meaning that you can only call suspend functions inside of another suspend function.

fun Example(a: () -> Unit, b: suspend () -> Unit) {
   a() // allowed
   b() // NOT allowed
}
 
suspend 
fun Example(a: () -> Unit, b: suspend () -> Unit) {
   a() // allowed
   b() // allowed
}

Composable works the same way. This is because there’s a calling context object that we need to thread through all of the invocations.

fun Example(a: () -> Unit, b: @Composable () -> Unit) {
   a() // allowed
   b() // NOT allowed
}
 
@Composable 
fun Example(a: () -> Unit, b: @Composable () -> Unit) {
   a() // allowed
   b() // allowed
}

what is this calling context thing that we’re passing around and why do we need to do it?

We call this object the Composer. The implementation of the Composer contains a data structure that is closely related to a Gap Buffer. This data structure is commonly used in text editors.

Let’s look at an example of a counter.

@Composable
fun Counter() {
 var count by remember { mutableStateOf(0) }
 Button(
   text="Count: $count",
   onPress={ count += 1 }
 )
}

This is the code that we would write, but let’s look at what the compiler does.

When the compiler sees the Composable annotation, it inserts additional parameters and calls into the body of the function.

First, the compiler adds a call to the composer.start method and passes it a compile time generated key integer.

fun Counter($composer: Composer) {
 $composer.start(123)
 var count by remember { mutableStateOf(0) }
 Button(
   text="Count: $count",
   onPress={ count += 1 }
 )
 $composer.end()
}

LazyRow or LazyColumns for instance have content with LazyListScope receiver that is not annotated with @Composable which doesn't let you to call other Composables

@Composable
fun LazyColumn(
    // Rest of params
     // Non Composable scope
    content: LazyListScope.() -> Unit
) {
  // Implementation details
}

LazyColumn(){ // LazyListScope
   // You can't add Composable here because this scope is LazyListScope
   // which is not annotated with @Composable, and returns compiler error
   // Row(){}
   
   items(100){
       Row() {
           
       }
   }
}

You can't call Composable functions from LazyListScope but you can call inside items because its content is @Composable function.

fun item(
    key: Any? = null,
    contentType: Any? = null,
    content: @Composable LazyItemScope.() -> Unit
) {
    error("The method is not implemented")
}

You need a scope with @Composable annotation to call other @Composable functions.

It's not mandatory for Composable functions to be UI functions. Some functions such as SideEffect or LaunchedEffect are used to run when your Composable function enters composition successfully.

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(
    effect: () -> Unit
) {
    currentComposer.recordSideEffect(effect)
}

Or not only to check when it enters composition also to check when it exits with DisposableEffect

@Composable
private fun NonUIComposableSample() {

    val context = LocalContext.current

    var counter by remember { mutableStateOf(0) }
    var color by remember { mutableStateOf(Color.Red) }

    if (counter in 3..5) {
        DisposableEffect(Unit) {

            Toast.makeText(context, "Entering Composition counter: $counter", Toast.LENGTH_SHORT).show()
            color = Color.Yellow
            onDispose {
                color = Color.Green
                Toast.makeText(context, "Exiting Composition counter: $counter", Toast.LENGTH_SHORT).show()
            }
        }
    }

    Button(onClick = { counter++ }) {
        Text("Counter: $counter", color = color)
    }
}

In this example Block with DisposableEffect enters composition when counter is 3 stay in composition while and exits when condition is not met anymore.

Also produceState is another @Composable non ui function launches a coroutine scoped to the Composition that can push values into a returned State. Use it to convert non-Compose state into Compose state, for example bringing external subscription-driven state such as Flow, LiveData, or RxJava into the Composition.

Compose is a data structure holds all of the objects from the composition, the entire tree in execution order, effectively a depth first traversal of the tree.

Upvotes: 13

Related Questions