https://kotlinlang.org logo
#compose
Title
# compose
s

shikasd

02/28/2020, 2:16 AM
Hi, I am experimenting with
Draw
function and transitions currently and have noticed that child composable get recomposed every time transition state changes. Is it possible to avoid child function recomposition? Example:
Copy code
val child = @Composable() { println("Hello") } 
Transition(definition) { state ->
    val x = state[X]
    Draw(child) {
        canvas.save()
        drawChildren()
        canvas.restore()
    }
}
s

Sean McQuillan [G]

02/28/2020, 5:37 AM
We're currently looking at when lambdas need to trigger recomposition (currently they do it too much). In this case, since it's a composable lambda with no arguments or captures I'm not sure why it's actually recomposing (ping @Leland Richardson [G] on that) Here's a quick (complete) repro:
Copy code
@Composable
private fun RecomposeLots() {
    val child = @Composable() { println("Recomposing child") }
    var currentTarget by state { 0.1f }
    Container {
        val animatedFloat = animate(currentTarget) { final ->
            currentTarget = final + 1 // keep it animating forever
        }
        println("Recomposing")
        Draw(child) { canvas, parentSize ->
            println("Drawing")
            canvas.save()
            drawChildren()
            canvas.restore()
        }
    }
}
If you swap that inline Draw out for a invocation of a named function composable, you'll get the behavior you want in today's compiler:
Copy code
@Composable
private fun RecomposeLots() {
    val child = @Composable() { println("Recomposing child") }
    var currentTarget by state { 0.1f }
    Container {
        val animatedFloat = animate(currentTarget) { final ->
            currentTarget = final + 1 // keep it animating forever
        }
        println("Recomposing")
        DrawButDontRecompose(child)
    }
}

@Composable
private fun DrawButDontRecompose(child: @Composable() () -> Unit) {
    Draw(child) { canvas, parentSize ->
        println("Drawing")
        canvas.save()
        drawChildren()
        canvas.restore()
    }
}
That's a bit of a bit of a workaround the current compiler though, and the behavior of lambdas and recomposition is expected to change (for the better)
l

Leland Richardson [G]

02/28/2020, 8:34 AM
this is kind of n interesting case. sean is right about the optimizations we are making but in this case Draw not being skipped in that way is kind of intentional. Draw is an inline composable and so we just execute the children as if they were an inline block. You can think of it like
Copy code
startNode(DrawNode(onDraw=myDrawCall)
child()
endNode()
So in this case the children executing every time is intentional. For what it’s worth, the draw tag itself just got removed today and the equivalent to what you’re trying to do would be done with a draw modifier. But other composables will be similarly inline, like for example Row and Column will likely be inline soon as well.
s

shikasd

02/28/2020, 12:11 PM
wow, thank you for the detailed replies 🙂 I also tried subclassing
DrawModifier
some time ago, but got similar behaviour where children got recomposed. I will try to reproduce it today :)
Essentially, that's how I tried it:
Copy code
@Composable
fun TestComposable() {
    println("Recomposed")
    Container(expanded = true, modifier = MyDrawModifier()) {
        ColoredRect(color = Color.Black)
    }
}

@Composable
fun MyDrawModifier(): DrawModifier {
    val currentTarget = animatedFloat(initVal = 0f)
    currentTarget.animateTo(1f, anim = TweenBuilder<Float>().apply { this.duration = 1000 })
    return remember {
        object : DrawModifier {
            override fun draw(density: Density, drawContent: () -> Unit, canvas: Canvas, size: PxSize) {
                canvas.save()
                canvas.translate(0f, size.height.value * currentTarget.value)
                drawContent()
                canvas.restore()
            }
        }
    }
}
When
TestComposable
is added, it is recomposing for each frame 🙂
Ah, if you wrap it with a similar function as Sean suggested, it works just fine! Thanks a lot!
My final code if someone is interested:
Copy code
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                DrawNoInlined {
                    TestComposable()
                }
            }
        }
    }
}

@Composable
fun TestComposable() {
    println("Recomposed")
    Container(expanded = true) {
        ColoredRect(color = Color.Black)
    }
}

@Composable
fun MyDrawModifier(): DrawModifier {
    val currentTarget = animatedFloat(initVal = 0f)
    currentTarget.animateTo(1f, anim = TweenBuilder<Float>().apply { this.duration = 1000 })
    return remember {
        object : DrawModifier {
            override fun draw(density: Density, drawContent: () -> Unit, canvas: Canvas, size: PxSize) {
                canvas.save()
                canvas.translate(0f, size.height.value * currentTarget.value)
                drawContent()
                canvas.restore()
            }
        }
    }
}

@Composable
fun DrawNoInlined(f: @Composable() () -> Unit) {
    Container(expanded = true, modifier = MyDrawModifier()) {
        f()
    }
}
💯 1