Lambda function used as input argument causes recomposition - android

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/

Related

Jetpack compose when to create a new lambda instance in Composable?

Please see my code:
#Composable
fun RecomposeLambdaTest() {
var state by remember {
mutableStateOf("1")
}
val stateHolder = remember {
StateHolder()
}
Column {
Button(onClick = {
state += "1"
}) {
Text(text = "change the state")
}
OuterComposable(state = state) {
stateHolder// just a reference to the instance outer the scope
}
}
}
#Composable
fun OuterComposable(state: String, onClick: () -> Unit) {
LogUtil.d("lambda hashcode: ${onClick.hashCode()}")
Column {
Text(text = state)
Button(onClick = onClick) {
Log.d("Jeck", "compose 2")
Text(text = "Text")
}
}
}
//#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always different! It means that a new lambda instance is created when recompose, everytime
and I uncomment the code in StateHolder and make it look like:
#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always the same! It means that when recompose, Composer reuse the lambda
So what' s under the hood?
Edit:
Ok, make it easier, Let's change the code like this:
val stateHolder = remember {
2
}
the result is lambda is reused.
make val to var, the lambda is created when every recompose.
So I think I know that: If the lambda refenence a valuable outer scope and the valuable is not stable, recreate lambda every time.
So the question is:
Why Compose compiler do this?
Why Compiler think the StateHolder before is not stable, it only contains a private var!?
An author met the same question, here is his article——6 Jetpack Compose Guidelines to Optimize Your App Performance
He said, private property still affact stability, it seems it is a Google team's choice.

Android Compose how to update other Composable function better way?

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.

How can I use LaunchEffect in the AndroidView in the Jetpack Compose?

I want to use a LaunchEffect to the AndroidView for collecting data from the ViewModel Stateflow but I get an error. how can I fix it?
You can run the LaunchedEffect only inside a #Composable function, this means that your lamba should me annotated with #Composable () -> Unit in order to be compatibile. But I'm not pretty sure that is a good practice.
You are trying to use composable LaunchedEffect not inside composable scope.
Move launched effect outside of getMapAsync.
You can do something like.
#Composable
fun MapViewContainer {
...
var mapIsReady by remember { mutableStateOf(false) }
...
mapView.getMapAsync {
mapIsReady = true
...
}
...
if (mapIsReady) {
// do some compose things
}
}
}

Why need the author to add the keyword remember in this #Composable?

The Code A is from the project ThemingCodelab, you can see full code here.
I think that the keyword remember is not necessary in Code A.
I have tested the Code B, it seems that I can get the same result just like Code A.
Why need the author to add the keyword remember in this #Composable ?
Code A
#Composable
fun Home() {
val featured = remember { PostRepo.getFeaturedPost() }
val posts = remember { PostRepo.getPosts() }
MaterialTheme {
Scaffold(
topBar = { AppBar() }
) { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
item {
Header(stringResource(R.string.top))
}
item {
FeaturedPost(
post = featured,
modifier = Modifier.padding(16.dp)
)
}
item {
Header(stringResource(R.string.popular))
}
items(posts) { post ->
PostItem(post = post)
Divider(startIndent = 72.dp)
}
}
}
}
}
Code B
#Composable
fun Home() {
val featured =PostRepo.getFeaturedPost()
val posts = PostRepo.getPosts()
...//It's the same with the above code
}
You need to use remember to prevent recomputation during recomposition.
Your example works without remember because this view will not recompose while you scroll through it.
But if you use animations, add state variables or use a view model, your view can be recomposed many times(when animating up to once a frame), in which case getting data from the repository will be repeated many times, so you need to use remember to save the result of the computation between recompositions.
So always use remember inside a view builder if the calculations are at least a little heavy, even if right now it looks like the view is not gonna be recomposed.
You can read more about the state in compose in documentation, including this youtube video, which explains the basic principles.

Why recomposition happens when call ViewModel in a callback?

I completely confused with compose conception.
I have a code
#Composable
fun HomeScreen(viewModel: HomeViewModel = getViewModel()) {
Scaffold {
val isTimeEnable by viewModel.isTimerEnable.observeAsState()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
) {
Switch(
checked = isTimeEnable ?: false,
onCheckedChange = {
viewModel.setTimerEnable(it)
},
)
Clock(viewModel.timeSelected.value!!) {
viewModel.setTime(it)
}
}
}
}
#Composable
fun Clock(date: Long, selectTime: (date: Date) -> Unit) {
NumberClock(Date(date)) {
val time = SimpleDateFormat("HH:mm", Locale.ROOT).format(it)
Timber.d("Selected time: time")
selectTime(it)
}
}
Why Clock widget recomposes when I tap switch. If I remove line selectTime(it) from Clock widget callback recomposition doesn't happen.
Compose version: 1.0.2
This is because in terms of compose, you are creating a new selectTime lambda every time, so recomposition is necessary. If you pass setTime function as a reference, compose will know that it is the same function, so no recomposition is needed:
Clock(viewModel.timeSelected.value!!, viewModel::setTime)
Alternatively if you have more complex handler, you can remember it. Double brackets ({{ }}) are critical here, because you need to remember the lambda.
Clock(
date = viewModel.timeSelected.value!!,
selectTime = remember(viewModel) {
{
viewModel.setTimerEnable(it)
}
}
)
I know it looks kind of strange, you can use rememberLambda which will make your code more readable:
selectTime = rememberLambda(viewModel) {
viewModel.setTimerEnable(it)
}
Note that you need to pass all values that may change as keys, so remember will be recalculated on demand.
In general, recomposition is not a bad thing. Of course, if you can decrease it, you should do that, but your code should work fine even if it is recomposed many times. For example, you should not do heavy calculations right inside composable to do this, but instead use side effects.
So if recomposing Clock causes weird UI effects, there is probably something wrong with your NumberClock that cannot survive the recomposition. If so, please add the NumberClock code to your question for advice on how to improve it.
This is the intended behaviour. You are clearly modifying the isTimeEnabled field inside your viewmodel when the user toggles the switch (by calling vm.setTimeenabled). Now, it is apparent that the isTimeEnabled in your viewmodel is a LiveData instance, and you are referring to that instance from within your Composable by calling observeAsState on it. Hence, when you modify the value from the switch's onValueChange, you are essentially modifying the state that the Composable depends on. Hence, to render the updated state, a recomposition is triggered

Categories

Resources