kotlinandroid-jetpack-composeanti-patternsnested-functioncomposable

Whether to use nested function for better readability in composables or not?


When using Jetpack Compose, there are some scenarios where there is a piece of code that does a specific job but since it is calling lambda parameters of the composable, it is not possible to fully extract an independent function from it.

In this situation, a method that comes to mind is having some nested functions (as provided in the following example) that each do a specific job.

@Composable
fun MyComposable(
  onUpdate: () -> Unit,
) {
    // ... other composable code ...

    // Local function
    fun nestedFunction() {
        // logic
        ...
        onUpdate()
    }

    LaunchedEffect(key = x) {
        // Call the local function
        nestedFunction()
    }
  

    // ... other composable code ...
}

I was wondering if this solution is an antipattern or not. Or maybe are there any other solutions?


Solution

  • It's not an anti-pattern it's implemented in Slider code as almost as in your question. Nested function is called from callback but you can do it as in your question as well.

    https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt;drc=ef8e9f60d94a9604380d3a00a18425f999fabcda;l=192

    Some code omitted it's like this in Slider

    @Composable
    fun Slider(
        value: Float,
        onValueChange: (Float) -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
        /*@IntRange(from = 0)*/
        steps: Int = 0,
        onValueChangeFinished: (() -> Unit)? = null,
        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
        colors: SliderColors = SliderDefaults.colors()
    ) {
    
        val onValueChangeState = rememberUpdatedState(onValueChange)
    
        BoxWithConstraints(
            modifier
                .minimumInteractiveComponentSize()
                .requiredSizeIn(minWidth = ThumbRadius * 2, minHeight = ThumbRadius * 2)
                .sliderSemantics(
                    value,
                    enabled,
                    onValueChange,
                    onValueChangeFinished,
                    valueRange,
                    steps
                )
                .focusable(enabled, interactionSource)
        ) {
       
    
            fun scaleToUserValue(offset: Float) =
                scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
    
            fun scaleToOffset(userValue: Float) =
                scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
    
            val scope = rememberCoroutineScope()
            val rawOffset = remember { mutableFloatStateOf(scaleToOffset(value)) }
            val pressOffset = remember { mutableFloatStateOf(0f) }
    
            val draggableState = remember(minPx, maxPx, valueRange) {
                SliderDraggableState {
                    rawOffset.floatValue = (rawOffset.floatValue + it + pressOffset.floatValue)
                    pressOffset.floatValue = 0f
                    val offsetInTrack = rawOffset.floatValue.coerceIn(minPx, maxPx)
                    onValueChangeState.value.invoke(scaleToUserValue(offsetInTrack))
                }
            }
    
        }
    }