ursus
12/04/2025, 9:27 PMParticle(val position: Offset, val velocity: Offset, val alpha: Float)
when I'm updating a particle, I'm doing it via
var particle by remember { mutableStateOf(Particle(..))
..
particle = particle.copy(position = ... )
Works, but since this is a sim, that's recalculated every frame, I don't want to create instances on every frame
So to avoid it, I want to have a mutable model
This is what I came up with
class Particle(
initialPosition: Offset,
initialVelocity: Offset,
val alpha: Float
) {
var position: Offset by mutableStateOf(initialPosition)
var velocity: Offset by mutableStateOf(initialVelocity)
}
...
particle.position = ...
How does this look? Is it idiomatic compose?
Do I need the @Stable annotation or is this fine?romainguy
12/04/2025, 9:33 PMromainguy
12/04/2025, 9:33 PMromainguy
12/04/2025, 9:34 PMmutableStateOf())ursus
12/04/2025, 9:35 PMdata class Particles(positions: Array<Offset>, .. )
but then if I just particles.positions[i] = .., how will compose recompose?romainguy
12/04/2025, 9:36 PMromainguy
12/04/2025, 9:36 PMromainguy
12/04/2025, 9:36 PMromainguy
12/04/2025, 9:36 PMursus
12/04/2025, 9:39 PMParticles , gotcha
btw why would my approach not be great? Obviously there's more overhead, but aren't the recompositions batched somehow to vsync? i.e. if I update bunch of things in a loop, I'll only get one recomposition/redraw, no?romainguy
12/04/2025, 9:39 PMromainguy
12/04/2025, 9:40 PMromainguy
12/04/2025, 9:40 PMursus
12/04/2025, 9:41 PMromainguy
12/04/2025, 9:52 PMromainguy
12/04/2025, 9:53 PMromainguy
12/04/2025, 9:53 PMmutableStateOf as you are introducing at least another pointer chase)ursus
12/04/2025, 10:19 PMromainguy
12/04/2025, 10:24 PMromainguy
12/04/2025, 10:24 PMArray<Particle> always require 2 pointer jumps to get to the particleromainguy
12/04/2025, 10:25 PMParticle[] where all the data is stored directly in the array, so only one dereferenceursus
12/04/2025, 10:25 PMromainguy
12/04/2025, 10:26 PMromainguy
12/04/2025, 10:27 PMromainguy
12/04/2025, 10:27 PMOffset is overall pretty efficient, in your case you might want to just store the positions as float arrays so you don't have to pack/unpack the data on every read/writeromainguy
12/04/2025, 10:28 PMOffset the most it could be, but it's still a tiny bit of overheadursus
12/04/2025, 10:28 PMromainguy
12/04/2025, 10:28 PMromainguy
12/04/2025, 10:28 PMOffset is technically a primitive btw)ursus
12/04/2025, 10:30 PMromainguy
12/04/2025, 10:30 PMromainguy
12/04/2025, 10:31 PMromainguy
12/04/2025, 10:31 PMromainguy
12/04/2025, 10:31 PMursus
12/04/2025, 10:32 PMromainguy
12/04/2025, 10:32 PMromainguy
12/04/2025, 10:32 PMursus
12/04/2025, 10:32 PMursus
12/04/2025, 10:33 PMursus
12/04/2025, 10:34 PMursus
12/04/2025, 10:35 PMromainguy
12/04/2025, 10:35 PMromainguy
12/04/2025, 10:35 PMursus
12/04/2025, 10:36 PMromainguy
12/04/2025, 10:37 PMromainguy
12/04/2025, 10:37 PMromainguy
12/04/2025, 10:37 PMromainguy
12/04/2025, 10:38 PMursus
12/04/2025, 10:38 PMursus
12/04/2025, 10:41 PMromainguy
12/04/2025, 10:45 PMromainguy
12/04/2025, 10:46 PMromainguy
12/04/2025, 10:46 PMromainguy
12/04/2025, 10:46 PMromainguy
12/04/2025, 10:47 PMromainguy
12/04/2025, 10:47 PMursus
12/04/2025, 10:50 PMromainguy
12/04/2025, 10:52 PMromainguy
12/04/2025, 10:53 PMursus
12/04/2025, 10:53 PMromainguy
12/04/2025, 10:55 PMromainguy
12/04/2025, 10:55 PMromainguy
12/04/2025, 10:55 PMursus
12/04/2025, 10:58 PMLaunchedEffect(Unit) {
var lastFrameTimestamp = 0L
while (isActive) {
withFrameMillis { frameTimestamp ->
val dt = if (lastFrameTimestamp == 0L) {
0f
} else {
(frameTimestamp - lastFrameTimestamp) / 1000f
}
updateParticles(particles, dt)
lastFrameTimestamp = frameTimestamp
}
}
}
and I'd have updateParticles run on gpu; or rather, to send the results to host via a coroutine..somehow
or would you rather block on a cpu thread?ursus
12/04/2025, 11:00 PMromainguy
12/04/2025, 11:00 PMursus
12/04/2025, 11:01 PMromainguy
12/04/2025, 11:02 PMromainguy
12/04/2025, 11:02 PMromainguy
12/04/2025, 11:02 PMursus
12/04/2025, 11:03 PMromainguy
12/04/2025, 11:03 PMwithFrameNanos to sync with vsyncromainguy
12/04/2025, 11:03 PMromainguy
12/04/2025, 11:04 PMromainguy
12/04/2025, 11:04 PMromainguy
12/04/2025, 11:04 PMursus
12/04/2025, 11:06 PMCanvas(Modifier.fillMaxSize()) {
for (particle in particles) {
val absolutePosition = Offset(
x = particle.position.x * size.width,
y = particle.position.y * size.height
)
drawCircle(
color = androidx.compose.ui.graphics.Color.Magenta.copy(alpha = particle.alpha),
center = absolutePosition,
radius = 2f
)
}
}
compose will not manage say 1M of the drawCircle calls on each frame?romainguy
12/04/2025, 11:20 PMursus
12/04/2025, 11:20 PMursus
12/04/2025, 11:40 PMromainguy
12/04/2025, 11:51 PMursus
12/04/2025, 11:51 PMromainguy
12/04/2025, 11:52 PMromainguy
12/04/2025, 11:52 PM