The following code is from the project.
I find that fun MainScreen() add #Composable, and fun launchDetailsActivity doesn't add #Composable.
It make me confused. I think all function which apply to Compose should to add #Composable, why doesn't fun launchDetailsActivity add #Composable?
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
ProvideWindowInsets {
ProvideImageLoader {
CraneTheme {
MainScreen(
onExploreItemClicked = { launchDetailsActivity(context = this, item = it) }
)
}
}
}
}
}
}
#Composable
fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
...
}
fun launchDetailsActivity(context: Context, item: ExploreModel) {
context.startActivity(createDetailsActivityIntent(context, item))
}
Function with #Composable is not just a function it tells the compose compiler that this is a UI element. Take this data and build a Widget with it.
So you have to determine when you will add #Composable based on whether this function draws something in the UI or not. In the non compose world you can think of this function like a View.
For example, this function takes a parameter name and builds a Text widget with the text "Hello $name" which you can see in the UI.
#Composable
fun Greeting(name: String) {
Text("Hello $name")
}
But
fun getName(firstName: String, lastName: String): String {
return "$firstName $lastName"
}
This function is not a composable function. It is not annotated with #Composable because it is not a widget, it shouldn't render anything in the UI. It just takes two parameters, Concatenates them, and returns the String.
In your case, MainScreen is the function that is rendering the Main screen of your app so it is a UI element. But function launchDetailsActivity doesn't draw anything in the UI. It just navigates from one activity to another activity.
Few things to remember:
Function with #Composable doesn't return anything.
You can't call a composable function from a non-composable function.
Unlike non-composable function composable function start with an Uppercase letter.
You can read this doc for details https://developer.android.com/jetpack/compose/mental-model
You need to mark view builder functions with #Composable, to be directly called from an other #Composable.
If you have a side effect function, it shouldn't be called directly from composable. It can be called from touch handlers, like click in your example, or using a side effect, like LaunchedEffect. Check out more about what's side effect in documentation.
#Composable
fun SomeView() {
// this is directly called from view builder and should be marked with #Composable
OtherView()
LaunchedEffect(Unit) {
// LaunchedEffect a side effect function, and as it's called
// from LaunchedEffect it can be a suspend fun (optionally)
handleLaunchedEffect()
}
Button(onClick = { handleButtonClick() }) { // or onClick = ::handleButtonClick
}
}
#Composable
fun OtherView() {
}
suspend fun handleLaunchedEffect() {
}
fun handleButtonClick() {
}
You should use #Composable if you are using calling another function annotated with #Composable that's it pretty simple.
Generally all #Composable functions starts with uppercase letter but some also start with lowercase like everything that starts with remember
So when you are using these functions inside another function you need to use #Composable else even android studio will yell at you because composable function can be invoked from another composable.
Related
Consider snippet below
fun doSomething(){
}
#Composable
fun A() {
Column() {
val counter = remember { mutableStateOf(0) }
B {
doSomething()
}
Button(onClick = { counter.value += 1 }) {
Text("Click me 2")
}
Text(text = "count: ${counter.value}")
}
}
#Composable
fun B(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("click me")
}
}
Now when pressing "click me 2" button the B compose function will get recomposed although nothing inside it is got changed.
Clarification: doSomething is for demonstration purposes. If you insist on having a practical example you can consider below usage of B:
B{
coroutinScope.launch{
bottomSheetState.collapse()
doSomething()
}
}
My questions:
Why this lamda function causes recomposition
Best ways to fix it
My understanding of this problem
From compose compiler report I can see B is an skippable function and the input onClick is stable. At first I though its because lambda function is recreated on every recomposition of A and it is different to previous one. And this difference cause recomposition of B. But it's not true because if I use something else inside the lambda function, like changing a state, it won't cause recomposition.
My solutions
use delegates if possible. Like viewmode::doSomething or ::doSomething. Unfortunately its not always possible.
Use lambda function inside remember:
val action = remember{
{
doSomething()
}
}
B(action)
It seems ugly =)
3. Combinations of above.
When you click the Button "Click me 2" the A composable is recomposed because of Text(text = "count: ${counter.value}"). It happens because it recompose the scope that are reading the values that can change.
If you are using something like:
B {
Log.i("TAG","xxxx")
}
the B composable is NOT recomposed clicking the Button "Click me 2".
If you are using
B{
coroutinScope.launch{
Log.i("TAG","xxxx")
}
}
the B composable is recomposed.
When a State is read it triggers recomposition in nearest scope. And a scope is a function that is not marked with inline and returns Unit.
To use a coroutinScope you have to use rememberCoroutineScope that is a composable inline function. The the body of inline composable functions are simply copied into their call sites, such functions do not get their own recompose scopes.
To avoid it you can use:
B {
Log.i("TAG","xxxx")
}
and
#Composable
fun B(onClick: () -> Unit) {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
onClick()
}
}
) {
Text(
"click me ",
)
}
}
Sources and credits:
Thracian's answer: Jetpack Compose Smart Recomposition
What is “donut-hole skipping” in Jetpack Compose? post: https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose
scoped recomposition: https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78
You can use the LogCompositions composable described in the 2nd post to check the recomposition in your code.
Generally speaking, if you are using a property inside a lambda function that is unstable, it causes the child compose function unskippable and thus gets recomposed every time its parent gets recomposed. This is not something easily visible and you need to be careful with it. For example, the bellow code will cause B to get recomposed because coroutinScope is an unstable property and we are using it as an indirect input to our lambda function.
fun A(){
...
val coroutinScope = rememberCoroutineScope()
B{
coroutineScope.launch {
doSomething()
}
}
}
To bypass this you need to use remember around your lambda or delegation (:: operator). There is a note inside this video about it. around 40:05
There are many other parameters that are unstable like context. To figure them out you need to use compose compiler report.
Here is a good explanation about the why: https://multithreaded.stitchfix.com/blog/2022/08/05/jetpack-compose-recomposition/
We can write functional interfaces in Kotlin like this - function-interfaces
fun interface Sum {
fun add(a: Int, b: Int): Int
}
val sumImpl = Sum { a, b ->
return#Sum a + b
}
val testSum = sumImpl.add(4, 5)
How can we write Jetpack Composable function in same way? Below code is not working.
`
fun interface SampleText {
#Composable
fun text(data : String)
}
val textImpl = SampleText { data ->
return#SampleText #Composable { Text(data) }
}
#Composable
fun testText() = textImpl.text("Data")
I have tried this as well, but this also didn't work.
fun interface SampleText {
fun text(data : String) : #Composable () -> Unit
}
val textImpl = SampleText { data ->
#Composable { Text(data) }
}
#Composable
fun testText() = textImpl.text("Data")
The first version is not compiling in its lambda form because your interface function returns a Unit and your'e actually having a Type mismatch error, its just weird the compiler reports Internal Error when you try to return a #Composable annotated function, but the issue becomes clear if you simply return something like a String.
vs
To solve your first version, either you fully declare an object of the class like this (though its useless since you want a lambda version of your SAM interface not an actual object in the first place)
val textImpl = object: SampleText {
#Composable
override fun text(data: String) {
Text(data)
}
}
, but it will work just by simply calling the testText() function like this.
testText()
Or change it to your second version.
Now for your second version, since your interface returns a #Composable lambda, you have to invoke it as well in the call-site, making two function invocations to make it work,
testText()() // two invocations
first call invokes your testText() function, second pair of parenthesis invokes the #Composable lambda from your interface.
Or simply call .invoke()
testText().invoke() // just call the .invoke() of the returned composable lambda
Either of the implementations and calls display the text "Data"
I'm trying to make Kotlin's invoke operator a #Composable, everything works fine, until I add a parameter to it, which should have a default value. See the code below:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent{
Button()
}
}
}
object Button{
#Composable
operator fun invoke(text: String = "SomeText"){
println(text) // prints: null
}
}
When the operator is not annotated as #Composable the output is SomeText, as it should be.
Is this some error in Jetpack Compose, or am I missing something?
The behavior is the same on the latest stable Compose v 1.1.1 and on 1.2.0-beta01. Kotlin 1.6.21
Based on the info provided in the comments, I decided to answer:
I'll maybe think of something better, but off the top of my head, this is what you can do for now
enum class ButtonType {
Primary,
Secondary,
Tertiary
}
Return the correct type of Button
#Composable
fun MasterButton(type: ButtonType) {
when(type) {
primary -> PrimaryButton()
secondary -> SecondaryButton()
else -> TertiaryButton() // Must provide an 'else' branch
}
}
This will do the job for you.
CORRECT APPROACH I:
I just got the correct one the moment I started typing the first approach.
#Composable
fun ( #Composable () -> Unit ).Primary(...) {
PrimaryButton()
}
Make copies for every other button.
STRONG NOTICE: This is a RIDICULOUS way of "cleaning" up the code. Nobody should ever use anything remotely resembling this ever, but since that is just what the question is about, this is how you go about doing it. Know that this will attach an extension function called Primary(...) to every single #Composable function, and that cannot change. You can't apply it to select Composable(s) only, since this is basically just an extension function that I have applied on a general labmda, since 'extension functions for extension functions' are not something that exist as of now.
I am going to take this as your question (even though it is in the comments) and try to answer the way I achieve this.
What I'm trying to achieve is a way to clean up the namespace, so that
not all Composables are available as a top-level function. The general
idea is to group all flavors of let's say Buttons (Primary, Secondary,
Tertiary) to be Composables declared as a function of object Button.
But I would like to be able to use also this Button object as a
default Button (let it be Primary) in a Compose way, so just by using
it as it would be a function, thus invoke() operator. I would have
Button.Primary(), Button.Secondary() and Button() which would be an
"alias" for Button.Primary().
My implementation is quite simple,
Expose only one top-level Composable function to have a cleaner namespace.
Pass an argument that denotes the type of the required Composable, using a sealed class.
Button Type
sealed class MyIconButtonType(
open val typeName: String,
) {
data class Default(
override val typeName: String = "default",
) : MyIconButtonType(
typeName = typeName,
)
data class BorderIconButton(
override val typeName: String = "border",
// The variant specific attributes can be added here
val borderWidth: Int,
) : MyIconButtonType(
typeName = typeName,
)
}
Button (The only composable exposed to other files)
#Composable
fun MyTestIconButton(
onClickLabel: String,
modifier: Modifier = Modifier,
data: MyIconButtonType = MyIconButtonType.Default(),
onClick: () -> Unit,
content: #Composable () -> Unit,
) {
when (data) {
is MyIconButtonType.Default -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required
)
}
is MyIconButtonType.BorderIconButton -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required, also make sure to pass variant specific attributes here
)
}
}
}
Usage
// For default impl
MyTestIconButton(
// default parameters
) {
}
// For specific variants
MyTestIconButton(
// default parameters
data = MyIconButtonType.BorderIconButton(
borderWidth = 10,
),
) {
}
Note:
Data class requires at least one attribute. Use object if no attributes like the typeName are required.
Like this,
sealed class MyIconButtonType {
object Default : MyIconButtonType()
data class BorderIconButton(
val borderWidth: Int,
) : MyIconButtonType()
}
Kotlin concepts that are used for reference,
Sealed classes, data classes and objects
when statement
Visibility modifiers
Screenshot
I just wanna click button can log ComposeableB().or liek this , For example, if you click ComposableA, ComposableB will start an animation instead of updating the data.
Although with Compose it is generally recommended to pass events to the app logic (like the ViewModel) instead of to the app UI (Thinking in Compose), here's how your code could look like if you really need to do that:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
#Composable
fun ComposableA() {
var addLogEntry by remember { mutableStateOf(false) }
Column {
Button(onClick = {
addLogEntry = true
}) {
Text(text = "Log")
}
ComposableB(addLogEntry = addLogEntry) {
addLogEntry = false
}
}
}
#Composable
fun ComposableB(
addLogEntry: Boolean,
onLogEntryAdded: () -> Unit
) {
if (addLogEntry) {
Log.d("Shadowmeld", "onAddLogEntry")
onLogEntryAdded()
}
}
Here you are passing a function as a second parameter (onLogEntryAdded, in lambda expression format) to ComposableB. This passed lambda expression will be called from ComposableB to modify state in ComposableA.
I believe there are better ways of doing this, like ComposableB being declared inside ComposableA (hoisting state to ComposableA) or, if that is not an option, passing the Button onClick event to a ViewModel that both ComposableA and ComposableB can observe.
In the Android developer docs at the following web address: https://developer.android.com/jetpack/compose/mental-model#recomposition
There is a composable function which is given as the following:
#Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
It's said in the text that this produces an element which updates the number of times its been clicked every time it is clicked. However, looking at it, it seems to need a lambda function to do that.
When I try and put it into the SetContent function I get the following:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WorkTimerTheme {
Conversation(SampleData.conversationSample)
ClickCounter(clicks = 0) {
//Insert My Function Here
}
}
}
}
}
The comment //Insert My Function Here has been added by me. I presume that within this I have to put a Lambda which updates the clicks value of the composable, but I have no idea what to put. Does anyone know an acceptable way of writing this?
You need a MutableState to trigger recomposition and remember{} to keep previous value when recomposition occurred.
I asked a question about it and my question contains answer to your question.
#Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
) {
Text("I've been clicked $count times")
}
}
Thanks very much to #Thracian for linking a similar question. As the answer to mine is related yet slightly different I thought I would post my own.
The correct code is as follows:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val counterState = remember { mutableStateOf(0) }
WorkTimerTheme {
Conversation(SampleData.conversationSample)
ClickCounter(clicks = counterState.value) {
counterState.value++
}
}
}
}
}
As suggested I added a mutableState value which appears to remember the value of something at the last "recomposition", unless it is explicitly updated. If a mutablestate is explicitly updated that will trigger a recomposition (as noted in the answer to #Thracian's question).
A recomposition will redraw the Element.
In order to update the value at recomposition the number of times the button has been clicked must be stored in the mutablestate and passed to the Composable Function at each recomposition.
Using the Composable functions Lambda argument to affect the mutable state completes the loop, updating the mutablestate which then recomposes the button with the updated value.
That is the purpose of counterState.value++.
As suggested above for more information on this, try reading this documentation: https://developer.android.com/jetpack/compose/state#viewmodel-state
The video is related to what we're discussing here.