I am writing an audio playing component, i have a ...
# compose
z
I am writing an audio playing component, i have a mutableStateOf the track progress every 100 millis, however, as long as that state is updated i cannot tap the play\pause button that is getting re-rendered every 100 milli, how can i prevent the left square from getting rendered on every track progress mutable state change? all the mutable states are held in a viewModel class, that is passed to the audio record composable.
t
Rather than screenshots, it can be helpful to post code blocks,
Copy code
like this
Which makes it a bit easier for us to read your code. Can you show us where
trackProgress
is referenced? I can see the slider, but I can’t see why the Card would recompose, because I can’t see what code surrounds your
Card
z
So the way it is built is basically like this:
Copy code
@Composable
fun AudioTrackView(viewModel: VoiceRecordingViewModel){
    Row(...) {
        PlayPauseButton() { viewModel.clickAction() }
        Slider(
                value = viewModel.trackProgress,
                valueRange = 0f..duration,
                modifier = Modifier
                   .weight(1f)
                   .fillMaxWidth()
                onValueChange = { newValue ->
                    viewModel.goToTrackPosition(newValue)
                },
             )
        Text(...)
        TrashIcon()
    }
}
trackProgress
which is streamed from the view model is given as the value of the slider
t
OK. I’m trying to understand what could be causing recomposition of your
AudioTrackView
here. I don’t see why
viewModel.trackProgress
would cause this
Maybe we need to see the code surrounding
AudioTrackView
?
Could it be some side-effect of
viewModel.goToTrackPosition()
?
👀 1
z
hmmm, maybe... checking....
Nope, removing that makes not difference
t
OK. Can you show us the parent code of
AudioTrackView
?
z
I will try and summerzie it, one moment....
Copy code
fun VoiceRecordInput(viewModel: VoiceRecordingViewModel) {
    Column(...)
    {
        Text(...)
        Text(...)
        LaunchedEffect(viewModel) {
            snapshotFlow {
                viewModel.items.firstOrNull { it.state.isIdle && !it.state.targetState }
            }.collect {
                if (it != null) {
                    viewModel.cleanInvisibleItems()
                }
            }
        }
        LazyColumn(
            content = {
                items(
                    items = viewModel.items,
                    itemContent = { item ->
                        AudioTrackView(viewModel, item.audioTrack)
                    }
                )
            }
        )
        Box(...)
        Text(...)
        Button(...)
    }
}
Removing the LaunchedEffect was the first suspect but removing it does not solve the problem
t
OK. It’s not immediately clear to me what the cause is. But, to help you diagnose the issue, it’s worth figuring out where recomposition is happening. Is it just the AudioCard, or could it be the VoiceRecordingInput composable, or even the Composable above that, that is being re-composed every x milliseconds?
You can use logcat for this, rather than creating an animating square. Let me find a snippet..
If you add that to your code, and then add
LogCompositions
to your composables, with a different tag for each one, you can narrow down where your excessive recomposition is coming from.
Once you know that, we can then try to narrow it down a bit further
z
Great! thank you!
The recomposition occurs inside the
PlayPauseButton() { viewModel.clickAction() }
that contains an
AnimatedVisibility
component, i am still investigating but appreciate the help, i hep post my findings here.
t
Maybe it’s just recomposing as the ripple animates? (Just a guess, no idea)
It could be that the recomposition isn’t actually the cause of your problem. You said that you can’t tap the play/pause button?
I’m totally guessing now but, I don’t think you’re meant to pass
MutableInteractionSource()
, but instead use
Copy code
remember { MutableInteractionSource() }
Not sure if that could be the cause of the problem?
z
I had that thought too as soon as you called the ripple thing but no... 😂
Also, if i place a plain button there it has ripple by default but has no problem, i think it has to do with the
AnimatedVisibility
t
Where is the animated visibility? I don’t see it in the code blocks you posted above?
z
No, there is alot of code so i tried to post only relevant code as much as possible
Copy code
@ExperimentalAnimationApi
@Composable
private fun AnimatedVisibilityWrapper(
    modifier: Modifier,
    visible: Boolean,
    rotation: Float,
    onClick: () -> Unit,
    content: @Composable () -> Unit
) {
    LogCompositions(tag = "AnimatedVisibilityWrapper")
    AnimatedVisibility(
        visible = visible,
        enter = fadeIn(
            animationSpec = tween(delayMillis = 50, durationMillis = 300, easing = FastOutLinearInEasing)
        ),
        exit = fadeOut(
            animationSpec = tween(delayMillis = 50, durationMillis = 300, easing = FastOutLinearInEasing)
        )
    ) {
        Card(
            backgroundColor = Color(255, Random.nextInt(0, 255), Random.nextInt(0, 255), Random.nextInt(0, 255)),
            elevation = 0.dp,
            modifier = modifier.then(
                Modifier
                    .fillMaxSize()
                    .graphicsLayer { rotationZ = rotation }
                    .clickable(
                        interactionSource = MutableInteractionSource(),
                        indication = rememberRipple(
                            radius = 20.dp,
                            bounded = false
                        )
                    ) {
                        onClick()
                    },
            )
        ) { content() }
    }
}
Used like this
Copy code
AnimatedVisibilityWrapper(
                    modifier = Modifier.align(Center),
                    visible = !isPlaying,
                    rotation = animateFloatAsState(targetValue = if (isPlaying) 180f else 0f).value,
                    onClick = { viewModel.playClicked(audioTrack.id) }
                ) {
                    Icon(
                        painterResource(id = R.drawable.ic_play),
                        modifier = Modifier.align(Center),
                        contentDescription = "Localized description",
                        tint = colorResource(id = R.color.lemonade_pink)
                    )
                }
isPlaying
is
Copy code
val isPlaying = viewModel.playing(audioTrack.id)
t
i tried to post only relevant code as much as possible
Yes, fair enough haha
So, it’s kind of expected that recomposition is going to occur a lot then, since an animation is occurring
And the animation begins after you press play?
z
exactly, the pause icon animates away as the play icon animates in
This has to be it
will update
t
Hmm.. Ok. It’s a bit confusing, I know you have a lot of code, but with every step you seem to talk about a new piece of code that I haven’t seen yet 😅
So, it sounds like you have more than one of these AnimatedVisibilityWrappers? One that wraps a play button, and one that wraps a pause button?
z
Yea i thought that the answer would be something obvious i havnet seen.
Yea more then one, this is not a question for this chat then
I appreciate you help alot.
t
It might be something obvious yet, but we can’t see enough code.
Like it would be good to see both the buttons, and the parameters they depend on, and for you to mention which button you can’t press and under what conditions
But yeah, if you don’t feel comfortable sharing that code, no problem
z
If this still boggles me by next week i will post the whole code on some gist and upload here.
You gave me valubale tools though, thank you
t
Don’t be afraid to post a ton of code in this thread, I doubt anyone really minds.. it’s a thread so they only see it if they want to
z
I think it would just be alot more readbale as a gist on github or something
slack does not show vast amounts of code nicely
t
Paste away my friend. We’ll manage, that’s what code blocks are for!
z
but now it's all meesed up from my experiments, and i must also move on to other tasks, i promise i will post a solution or the full code here eventually.
🙌 1
*Update Extracting the Slider out to a private function prevents the AnimatedVisibility from recomposing I am not sure why. Here is the full code https://gist.github.com/zivkesten/c985971e61fd9fb98e884dc067cac1c1
m
when you click play/pause, does the viewmodel method gets called ?
z
There is a viewModel method that is called on playPause, but it does not trigger the trackProgress state, which is what causing the recomposition. But I want to know, why did you ask?.
m
so that progress is animating, and the square that is blinking is the clickable component, that will cause it to pause/play right ?
oh i was looking to the first screen. The component is the last one ok. You are saying you cannot click the play/pause when the progress is animating. I am asking if the click event goes through the viewModel or not
z
The progress bar is animating due to a state change from the viewModel, the blinking square was my indication that it is recomposing, even thought it has no reason too. While it is recomposing, I cannot click it, the viewModel method is not even called, that was my initial problem.
m
quick test:
Copy code
Surface(
    modifier = Modifier
        .fillMaxSize(),
    color = MaterialTheme.colors.background
) {
    var value by remember {
        mutableStateOf(0f)
    }
    LaunchedEffect(key1 = Unit) {
        while (true) {
            delay(100)
            value += 1f
        }
    }

    Row {
        Button(onClick = {
            Log.i("TAG", "Clicked")
        }) {
            Text(text = "Button: $value")
        }
        Slider(value = value, onValueChange = {}, valueRange = 0f..1000f)
    }
}
Quick test with a similar sample, it works as expected even when it is recomposing the button. hm .. interesting
maybe some animateVisibility bug, or card click bug idk =( having access to the full code would help. ViewModel code as well
but if you say the viewModel event is not reached, the problem must not be in the viewModel
wait. Maybe not it, but can you try to use the card click param, instead of the modifier created one =?
z
I appreciate the help, I left the computer by now, and am leaving for the weekend, but I will surly do that test you suggested on Sunday, and will update here! Thank you very much!
m
im just guessing
i once had problems with card modifier click, i didnt realise it had an overload for that. but i dont think thats it, just trying it out
z
Interesting, I will look into that as well.