dylan.kwon
dylan.kwon

Reputation: 551

Why this code is causing recomposition (jetpack compose)

I don't know why in my code the text where I type "hi" is always recomposed. The text was fixed to "hi", and onClick also maintained the instance using uiEvent remember.

Even if uiState.isLoading is changed, it is not related to Text, so I expected that recomposition would be skipped, but recomposition always occurs when Text is changed.

Here is my code:

UiState Data Class

data class UiState(
    val isLoading: Boolean
)

UiEvent Interface.

@Immutable
interface UiEvent {
    fun onClick()
}

Composable

@Composable
fun MyScreen(
    viewModel: MyViewModel = hiltViewModels()
) {
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    val uiEvent = remember {
       object: UiEvent {
            override onClick() {
                viewModel.action()
            }
       }
    }
    Text(  // why recomposition?
        modifier = Modifier.clickable(onClick = uiEvent::onClick),
        text = "hi"
    )
    if (uiState.isLoading) {
        Text(text = "isLoading")
    }

}

Upvotes: 8

Views: 3502

Answers (2)

appdev
appdev

Reputation: 9

I have made a simple layout with 3 texts:

  • Static text with on click
  • Static text
  • Dynamic text (with clicks count)

simple layout

This is how recompositions count looks after 3 clicks:

recompositions

Clickable Text is recomposed every click. Tested on Compose BOM 2023.08.00

Workaround is to wrap clickable modifier with remember but I am not sure if it should be default way to use clickable Modifier.

Upvotes: 0

chuckj
chuckj

Reputation: 29575

I assume what you are asking is why does Text not skip whenever MyScreen recomposes. The reason is a new of the clickable modifier is being created every time. Consider remembering the entire modifier such as:

@Composable
fun MyScreen(
    viewModel: MyViewModel = hiltViewModels()
) {
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    val modifier = remember(viewModel) {
        val event = object: UiEvent {
            override fun onClick() {
                viewModel.action()
            }
        }
        Modifier.clickable { event.onClick() }
    }
    Text(  // why recomposition?
        modifier = modifier,
        text = "hi"
    )
    if (uiState.isLoading) {
        Text(text = "isLoading")
    }
}

Note the addition of the viewModel as a parameter to the remember to ensure a new version of clickable() gets created if/when the viewModel changes.

The compose compiler plugin should recognize the :: syntax and do the remember for you, like it does for lambda syntax, but it doesn't right now. Also, the clickable() always returns a modifier that is not equal to the previous version for the same parameters, but that is going to be fixed in 1.5 (along with quite a number of other performance related fixes for clickable()). For now, you can work around these limitation by remembering the entire modifier.

Upvotes: 9

Related Questions