Search code examples
androidstateandroid-jetpack-composemodifier

Compose clickable modifier with interactionSource breaks


Updated - see in the bottom of the question.

We are trying to add content to a sticky topbar defined from each screen, and the solution works. But our custom modifier clickableWithScaleAnimation breaks when ever we add add a composable method to the stickyTopBar in ActualScreenComposable in the example below. It does just not scale or click, but if we add a simple .clickable {} after it, that will still work, so it is quite strange.

I suspect that the problem is related to the way of saving a composable method in a state, but it was working fine untill we spotted this exact problem. If we remove the stickyTopBar { ... } call it works, so it must be something with the was the state is set or handled. Any clues?

Scaffold(...) { 
    Column {
        NavigationGraph(...)
    }
}
    
    
fun NavigationGraph(...) {
    val stickyTopBarContent = remember { mutableStateOf<(@Composable () -> Unit)?>(null) }
    
    stickyTopBarContent.value?.let {
        it.invoke()
    }

    AnimatedNavHost {
        screenComposable(actuallScreenConfig, stickyTopBarContent)
        ...
    }
}

fun screenComposable(screenConfig, stickyTopBarContent, ...) {
    if (!screenConfig.supportsStickyTopBar) {
        stickyTopBarContent.value = null
    }
                
    ActualScreenComposable(stickyTopBar = { stickyTopBarContent.value = it }, ...)
}

fun ActualScreenComposable(stickyTopBar: (@Composable () -> Unit) -> Unit, ...) {
    stickyTopBar {
        StickyTopBar() // it even fails with no content in here
    }
    content
}

This is the modifier clickableWithScaleAnimation that does not work if the stickyTopBar { ... } is called.

fun content() {
    val coroutineScope = rememberCoroutineScope()
    Column(modifier.clickableWithScaleAnimation(
                coroutineScope = coroutineScope,
                scale = scale,
                scaleDownTo = 0.9f,
                animationDuration = 200,
                onClick = {
                    onSelected(...)
                }
            ))
         // .clickable { onSelected(...) } // this will work if added
            {
            ...
            }
        
}

fun Modifier.clickableWithScaleAnimation(
    interactionSource: MutableInteractionSource = MutableInteractionSource(),
    coroutineScope: CoroutineScope,
    scale: Animatable<Float, AnimationVector1D>,
    animationDuration: Int = 200,
    scaleDownTo: Float = 0.9f,
    onClick: () -> Unit
) = scale(scale.value)
    .clickable(interactionSource = interactionSource, indication = null) {
        coroutineScope.launch {
            scale.animateTo(scaleDownTo, animationSpec = tween(animationDuration))
            onClick()
            scale.animateTo(1f, animationSpec = tween(animationDuration))
        }
    }

We are using Compose 1.3.0

Update 1:

The problem is related to the clickable modifier. The clicked callback is never called. Our goal is just to remove the indication:

.clickable(interactionSource = interactionSource, indication = null) { ... } // fails
.clickable() { ... } // works

So there must be something in that specific modifier that somehow breaks - any clue what it could be?


Solution

  • It is possible to solve like this with a CompositionLocalProvider to hide the indication and then use the clickable modifier without arguments, but it does not explain the cause of the problem.

    fun content() {
        CompositionLocalProvider(LocalIndication provides NoIndication) {
        val coroutineScope = rememberCoroutineScope()
        Column(modifier.clickableWithScaleAnimation(
                    coroutineScope = coroutineScope,
                    scale = scale,
                    scaleDownTo = 0.9f,
                    animationDuration = 200,
                    onClick = {
                        onSelected(...)
                    }
                ))
       }     
    }
    
    object NoIndication : Indication {
        private object NoIndicationInstance : IndicationInstance {
            override fun ContentDrawScope.drawIndication() {
                drawContent()
            }
        }
    
        @Composable
        override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
            return NoIndicationInstance
        }
    }