Reputation: 2355
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
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