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

101 Views Asked by At

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?

2

There are 2 best solutions below

1
Thracian On BEST ANSWER

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))
            }
        }

    }
}
1
AtifSayings On

Using nested functions within a Compose composable function to organize and encapsulate logic is a common and acceptable practice. It's not considered an antipattern; in fact, it can improve the readability and maintainability of your code.

Jetpack Compose encourages the use of declarative UI with a focus on creating composable functions that are concise and focused on a single responsibility. However, there might be scenarios where the logic within a composable becomes complex or involves multiple steps.

In such cases, breaking down the logic into nested functions can be beneficial for several reasons:

  1. Readability: Nested functions can provide clear and descriptive names for each step of the logic, making it easier for others (or yourself) to understand the code.

  2. Encapsulation: By encapsulating logic in nested functions, you can keep the implementation details hidden within the composable, preventing the clutter of the main composable function.

  3. Reusability: If there are parts of the logic that might be reused in different places, you can extract those nested functions into separate utilities or extension functions for broader reuse.

Here's a simple example:

@Composable
fun MyComposable() {
    // ... other composable code ...

    val result = calculateResult()

    // ... use the result in UI ...
}

private fun calculateResult(): Int {
    val intermediateResult = doSomeCalculation()
    val finalResult = doAnotherCalculation(intermediateResult)
    return finalResult
}

private fun doSomeCalculation(): Int {
    // ... logic ...
}

private fun doAnotherCalculation(intermediateResult: Int): Int {
    // ... logic ...
}