The reason you can't click an ancestor Composable is not because it's under another, touch propagation goes from descendant to ancestor by default because of PointerEventPass which is Main.
The reason Modifier.clickable doesn't allow parent to get event is, it calls PointerEventChange.consume
but you can write your own click functions to propagate from child to parent or parent to child or consume conditionally.
You can check out this answer for more details
You are basically doing nothing on checkBoxChange that's why it doesn't change, set checkedState inside onCheckedChange
.
@Composable
fun CheckboxInRowWithText() {
val context = LocalContext.current
var checkedState by remember { mutableStateOf(false) }
Surface {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
Toast
.makeText(context, "Row clicked", Toast.LENGTH_SHORT)
.show()
checkedState = !checkedState
},
verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = checkedState, onCheckedChange = {
Toast.makeText(context, "Checkbox check", Toast.LENGTH_SHORT).show()
checkedState = it
}
)
Text(text = "Clicking this Row will toggle checkbox")
}
}
}
Also you can build it like this which will show ripple even when you click CheckBox.
@Composable
fun CheckBoxWithTextRippleFullRow(
label: String,
state: Boolean,
onStateChange: (Boolean) -> Unit
) {
// Checkbox with text on right side
Row(modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.clickable(
role = Role.Checkbox,
onClick = {
onStateChange(!state)
}
)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = state,
onCheckedChange = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = label)
}
}
Result

Click propagation sample
You can click below a Composable even with a Button
which is a Surface
with Modifier.clickable
under the hood.
Let's create our own custom touch only for the parent below Button
private fun Modifier.customTouch(
pass: PointerEventPass,
onDown: () -> Unit,
onUp: () -> Unit
) = this.then(
Modifier.pointerInput(pass) {
awaitEachGesture {
awaitFirstDown(pass = pass)
onDown()
waitForUpOrCancellation(pass)
onUp()
}
}
)
And assign it to parent with PointerEventPass.Initial with no consume call and will result as

@Preview
@Composable
private fun LookAheadLayoutTest() {
val context = LocalContext.current
Box(
modifier = Modifier
.customTouch(
pass = PointerEventPass.Initial,
onDown = {
Toast
.makeText(context, "Parent down", Toast.LENGTH_SHORT)
.show()
}, onUp = {
Toast
.makeText(context, "Parent up", Toast.LENGTH_SHORT)
.show()
}
)
.padding(bottom = 50.dp)
.fillMaxSize()
.padding(20.dp),
contentAlignment = Alignment.BottomCenter
) {
Button(
onClick = {
Toast.makeText(context, "Button is clicked", Toast.LENGTH_SHORT).show()
}) {
Text("Click Button")
}
}
}