Issue
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 their is 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")
}
}
}
Solution
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.
Answered By - Thracian
Answer Checked By - Mary Flores (JavaFixing Volunteer)