I am trying to build an interactive screen that ha...
# compose
p
I am trying to build an interactive screen that have a lot of reusable composables that fly on the screen with random offset. Video in thread 🧵 . Currently it works with
mutableStateListOf
where I am adding new items on each click. Then I call list.onEach { } to render new composable. I feel like this solution is not ideal, cause the list teorytically may have ♾️ elements. I tried to delete the items when animatino ends, but that is causing the view to recompose with some elements “inProgress” and introduce a lot of glitches. What is the proper way to fire these emojis?
Copy code
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            EmojiCannonTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    EmojiBox()
                }
            }
        }
    }
}

@Composable
fun EmojiBox() {
    Box(modifier = Modifier.fillMaxSize()) {
        val emojis = remember { mutableStateListOf<MyEmoji>() }
        val width = LocalConfiguration.current.screenWidthDp
        val height = LocalConfiguration.current.screenHeightDp

        Button(
            modifier = Modifier.align(Alignment.BottomCenter),
            onClick = {
                emojis.add(
                    MyEmoji(
                        id = UUID.randomUUID().toString(),
                        rotation = Random.nextInt(-90, 90).toFloat(),
                        offsetX = Random.nextInt(0, width).dp,
                        offsetY = Random.nextInt(height.div(2), height).dp
                    )
                )
            }) {
            Text(text = "FIRE!!")
        }

        emojis.forEach { emoji ->
            SingleEmojiContainer(item = emoji, onAnimationFinished = {  })
        }
    }
}

@Composable
fun SingleEmojiContainer(item: MyEmoji, onAnimationFinished: () -> Unit) {
    var visible by remember { mutableStateOf(true) }
    val offsetYAnimatable = remember { Animatable(0f) }
    val offsetXAnimatable = remember { Animatable(0f) }
    val rotationAnimatable = remember { Animatable(0f) }

    LaunchedEffect(Unit) {
        val offsetY = async { offsetYAnimatable.animateTo(1f, animationSpec = tween(1000)) }
        val offsetX = async { offsetXAnimatable.animateTo(1f, animationSpec = tween(1000)) }
        val rotation = async { rotationAnimatable.animateTo(1f, animationSpec = tween(1000)) }
        awaitAll(offsetX, offsetY, rotation)
        visible = false
        onAnimationFinished()
    }

    AnimatedVisibility(
        visible = visible,
        enter = fadeIn(animationSpec = tween(durationMillis = 300)),
        exit = fadeOut(animationSpec = tween(durationMillis = 2000)),
        modifier = Modifier
            .offset(
                x = item.offsetX.times(offsetXAnimatable.value),
                y = item.offsetY.times(offsetYAnimatable.value)
            )
            .rotate(rotationAnimatable.value.times(item.rotation))
    ) {
        Text(text = "\uD83E\uDEE0", fontSize = 40.sp)
    }
}

data class MyEmoji(
    val id: String,
    val rotation: Float,
    val offsetX: Dp,
    val offsetY: Dp
)
z
Give your emojis some sort of ID (e.g. just a monotonically increasing int counter), and then inside your
forEach
use the
key(id) { }
function. I think that should fix it
p
Working fine. Thanks!! I tried the key(id) before but without the stateList. I guest you need both 🙂
z
Yep. The state list is what tells the composition that things have changed, the key function allows the composition state (inside SingleEmojiContainer) to be moved around with its associated list item instead of shifted to whatever new item happens to have the same index after the change.