Search code examples
androidkotlinandroid-jetpack-composeandroid-jetpack

How to Prevent Recomposition of Entire UI When Using Weight in Jetpack Compose?


I'm building a calculator app in Jetpack Compose and facing an issue with recomposition. My layout has three main components: CalculatorDisplay, CalculatorAdvanced, and CalculatorPad. The layout uses weights for responsive design. However, when I toggle theisExpanded state using CalculatorExpandButton, the whole screen recomposes instead of only the relevant components (CalculatorAdvanced and CalculatorPad).

My goal:

  • Prevent CalculatorDisplay from recomposing unnecessarily.
  • Ensure only CalculatorAdvanced and CalculatorPad recompose when isExpanded changes.

What I've tried:

  • Using remember and derivedStateOf: I applied these to advancedWeight and padWeight to minimize state updates, but the entire UI still recomposes. (Maybe I did it incorrectly)
  • Inspecting with Layout Inspector: It confirms that pressing CalculatorExpandButton causes the whole PhonePortraitLayout to recompose.

Here's the code for the layout:

@Composable
fun CalculatorScreen(viewModel: CalculatorViewModel = viewModel()) {
    val context = LocalContext.current
    val activity = context as Activity
    val window = activity.window
    val windowSizeClass = rememberWindowSizeClass()

    val color = MaterialTheme.colorScheme.surfaceColorAtElevation(24.dp).toArgb()
    SideEffect {
        window.statusBarColor = color
    }

    val expandedStateHandler = remember {
        ExpandedStateHandler()
    }

    Box(modifier = Modifier.fillMaxSize()) {
        when {
            /** PHONE */
            windowSizeClass.isPhonePortrait -> PhonePortraitLayout(
                viewModel = viewModel,
                windowSizeClass = windowSizeClass,
                expandedStateHandler = expandedStateHandler
            )
        }
    }
}

class ExpandedStateHandler {
    var isExpanded by mutableStateOf(false)
        private set

    fun toggle() {
        isExpanded = !isExpanded
    }
}

@Composable
fun PhonePortraitLayout(
    viewModel: CalculatorViewModel,
    windowSizeClass: MyWindowSizeClass,
    expandedStateHandler: ExpandedStateHandler
) {
    val displayWeight = 0.3337f // Fixed weight for CalculatorDisplay
    val advancedWeight = if (expandedStateHandler.isExpanded) 0.1451f else 0.045f
    val padWeight = if (expandedStateHandler.isExpanded) 0.4f else 0.5f

    Column(
        verticalArrangement = Arrangement.spacedBy(8.dp),
        modifier = Modifier.padding(bottom = 8.dp)
    ) {
        // Stabilized CalculatorDisplay
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .weight(displayWeight)
        ) {
            CalculatorDisplay(
                viewModel = viewModel,
                modifier = Modifier.fillMaxSize()
            )
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .weight(advancedWeight)
        ) {
            Box(
                modifier = Modifier
                    .weight(3.1f) // Keeps internal proportions
                    .padding(start = 16.dp, end = 4.dp)
            ) {
                CalculatorAdvanced(
                    viewModel = viewModel,
                    windowSizeClass = windowSizeClass,
                    isExpanded = expandedStateHandler.isExpanded,
                    modifier = Modifier.fillMaxSize()
                )
            }

            Box(
                contentAlignment = Alignment.TopEnd,
                modifier = Modifier
                    .padding(start = 4.dp, end = 16.dp)
                    .weight(0.56f)
            ) {
                CalculatorExpandButton(
                    text = if (expandedStateHandler.isExpanded) "Adv" else "Basic",
                    onClick = { expandedStateHandler.toggle() }
                )
            }
        }

        Box(
            modifier = Modifier
                .fillMaxWidth()
                .weight(padWeight)
                .padding(horizontal = 16.dp)
        ) {
            CalculatorPad(
                viewModel = viewModel,
                windowSizeClass = windowSizeClass,
                modifier = Modifier.fillMaxSize()
            )
        }
    }
}


Solution

  • I think you need to make two changes:

    1. First, move the following code into the "PhonePortraitLayout" Composable:

      val expandedStateHandler = remember {
          ExpandedStateHandler()
      }
      
    2. If you want to prevent "CalculatorDisplay" from recomposing, move it out of the "PhonePortraitLayout" and place it in the "CalculatorScreen."