6

I'm using TextField to get user input and using stateflow to handle the text state/value in viewmodel.

The thing is everytime textfield value changes HomeContent() function get recomposed. Layout inspector output image My question is it ok the whole HomeContent() function is getting recompose just because of textfield value change or is there a way of avoiding function recomposition?

ViewModel

class MyViewModel() : ViewModel() {
    private val _nameFlow = MutableStateFlow("")
    val nameFlow = _nameFlow.asStateFlow()

    fun updateName(name: String) {
        _nameFlow.value = name
    }
}

MainActivity

class MainActivity : ComponentActivity() {
    private val myViewModel by viewModels<MyViewModel>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppArchitectureTheme {
                HelloScreen(myViewModel)
            }
        }
    }
}

HomeScreen

@Composable
fun HelloScreen(viewModel: MyViewModel) {
    val name = viewModel.nameFlow.collectAsState()
    HelloContent(
        provideName = { name.value },
        onNameChange = { viewModel.updateName(it) })
}

@Composable
fun HelloContent(
    provideName: () -> String,
    onNameChange: (String) -> Unit
) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello,",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = provideName(),
            onValueChange = { onNameChange(it) },
            label = { Text("Name") }
        )
        Button(
            onClick = {}
        ) {
            Text(text = "Dummy Button")
        }
    }
}
Simon Forsberg
  • 13,086
  • 10
  • 64
  • 108
Harsh
  • 71
  • 1
  • 4
  • You say that `HomeContent` is being recomposed, but you have one composable called `HomeScreen` and one called `HelloContent`. Neither one is called `HomeContent`. – Simon Forsberg May 24 '23 at 20:23

3 Answers3

11

This is indeed the expected behavior. Compose recomposes only the "nearest" scope. A scope is non-inline Composable function that returns Unit.

You can read answers about this in the links below. However, the difference between the question and answers in the link(s) is you also defer reading change which keeps Composables in between from being recomposed when the value inside the lambda changes.

Jetpack Compose Smart Recomposition

Why does mutableStateOf without remember work sometimes?

How can I launch recomposition when a specified Flow changed in Jetpack Compose?

If you change OutlinedTextField to

@Composable
private fun MyOutlinedTextField(
    provideName: () -> String,
    onNameChange: (String) -> Unit
) {
    OutlinedTextField(
        value = provideName(),
        onValueChange = { onNameChange(it) },
        label = { Text("Name") }
    )
}

this function will only be recomposed when the parameter it reads changes. If there were multiple MyOutlinedTextField, only the one that reads the respective value would change.

However, a subtle and very important difference between this and the answers in links that I provided, is deferring state-read by passing

provideName: () -> String instead of provideName: String

This defers state-reads from descendent Composables to only the one that reads this lambda. That's how you trigger recompositions only for MyOutlinedTextField scope.

If you update your function as below, you will see that it will again recompose the whole HelloContent scope including the Text inside the Column here,

@Composable
fun HelloContent(
    provideName: String,
    onNameChange: (String) -> Unit
) {
    Column(
        modifier = Modifier
            .background(getRandomColor())
            .padding(16.dp)
    ) {

        Column( Modifier
            .background(getRandomColor())) {
            Text(
                text = "Hello,",
                modifier = Modifier.padding(bottom = 8.dp),
            )
        }

        MyOutlinedTextField(provideName = provideName, onNameChange = onNameChange)

        Button(
            onClick = {}
        ) {
            Text(
                text = "Dummy Button", modifier = Modifier
                    .background(getRandomColor())
            )
        }
    }
}

This function recomposes the Column where the random color function is

fun getRandomColor(): Color {
    return Color(
        Random.nextInt(256),
        Random.nextInt(256),
        Random.nextInt(256),
        255
    )
}

Also, this is the tutorial I prepared which covers scoped recomposition and deferring reads. You can check out this example for reading offset, and padding changes.

https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials/blob/master/Tutorial1-1Basics/src/main/java/com/smarttoolfactory/tutorial1_1basics/chapter4_state/Tutorial4_7_3ComposePhases3.kt

enter image description here

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42
Thracian
  • 43,021
  • 16
  • 133
  • 222
0

Don't extract a val outside the HelloContent's calling site. Move the initialization logic to the lambda.

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42
0

The answer provided by @Thracian is correct, but I want to clarify this section:

Compose recomposes only the "nearest" scope. A scope is non-inline Composable function that returns Unit.

In your example, Column is an inline function, therefore the nearest scope non-inline Composable function is HelloContent and this is the reason why it gets recomposed.