Search code examples
androidkotlinandroid-jetpack-composeandroid-jetpack

Composable recomposing even with strong-skipping mode enabled


I have tested this using Kotlin 2.0.0, 2.0.10-RC and 1.9.20 just to be sure thisn't some weird Kotlin compiler issue. However, I am currently struggling to understand how lambdas affect the ability to skip recomposition of a component using said lambda. I have also generated Compose Compiler Metrics for my project for anyone interested in reproducing this problem but all of the components and classes were marked as skippable and stable. (even the lambdas)

My screen currently looks like this:

@Composable
fun CounterScreen(presenter: CounterPresenter) {
  val state = presenter.present()
  CounterContent(state)
}

@Composable
private fun CounterContent(state: CounterState) {
  // remember lambdas to ensure recomposition of buttons is skipped
  val increment = remember {
    { state.eventSink(CounterEvent.Increment) }
  }

  val decrement = remember {
    { state.eventSink(CounterEvent.Decrement) }
  }

  Scaffold(content = { paddingValues ->
    Box(
      modifier = Modifier.fillMaxSize(),
      contentAlignment = Alignment.Center
    ) {
      Column(
        modifier = Modifier.padding(paddingValues),
        horizontalAlignment = Alignment.CenterHorizontally,
      ) {
        TextButton(onClick = increment) {
          Text("Increment")
        }
        Text("Count: ${state.count}")
        Text(state.message)
        TextButton(onClick = decrement) {
          Text("Decrement")
        }
      }
    }
  })
}

The rest of my Presenter, State, Event setup looks like this:


sealed interface CounterEvent : Event {
  data object Increment : CounterEvent
  data object Decrement : CounterEvent
}

class CounterPresenter : Presenter<CounterState> {

  @Composable
  override fun present(): CounterState {
    var count by rememberSaveable { mutableIntStateOf(0) }
    val message by remember {
      derivedStateOf {
        when {
          count < 0 -> "Counter is less than 0"
          count > 0 -> "Counter is greater than 0"
          else -> "Counter is 0"
        }
      }
    }

    return CounterState(
      count = count,
      message = message,
      eventSink = { event ->
        when (event) {
          Increment -> count += 1
          Decrement -> count -= 1
        }
      }
    )
  }
}

data class CounterState(
  val count: Int,
  val message: String,
  override val eventSink: (CounterEvent) -> Unit,
) : State<CounterEvent>

Pressing the increment 10 times results in the following recomposition counts:

enter image description here

As you can see, the buttons are correctly skipped. However, enabling strong-skipping should theoretically allow me to remove the remember calls and pass in these lambdas directly and avoid recomposition too. This is not the case however. Removing the lambdas with strong-skipping mode enabled reusults in the following recomposition counts:

enter image description here

The buttons are now recomposed everytime the count is changed. This is the code for the screen I used, directly passing the lambdas to the buttons:

@Composable
private fun CounterContent(state: CounterState) {
  Scaffold(content = { paddingValues ->
    Box(
      modifier = Modifier.fillMaxSize(),
      contentAlignment = Alignment.Center
    ) {
      Column(
        modifier = Modifier.padding(paddingValues),
        horizontalAlignment = Alignment.CenterHorizontally,
      ) {
        TextButton(onClick = { state.eventSink(CounterEvent.Increment) }) {
          Text("Increment")
        }
        Text("Count: ${state.count}")
        Text(state.message)
        TextButton(onClick = { state.eventSink(CounterEvent.Decrement) }) {
          Text("Decrement")
        }
      }
    }
  })
}

Link to project: https://github.com/itsandreramon/mvp-compose


Solution

  • So apparently this is a known issue with the Circuit-pattern aka bundling events with the state. When enabling strong-skipping mode, I was able to avoid the remember calls while still skipping over the button recompositions using this simple trick thanks to Saurabh Arora:

    @Composable
    private fun CounterContent(state: CounterState) {
      val eventSink = state.eventSink // this works for some reason
      TextButton(onClick = { eventSink(CounterEvent.Increment) }) {
        Text("Increment")
      }
    }