Search code examples
androidandroid-jetpack-composeandroid-compose-textfield

Android Jetpack Compose - Composable Function get recomposed each time Text-field value changes


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

The thing is every time TextField's value changes the HomeContent() function get recomposed.
Layout inspector output image My question is, is it ok that the whole HomeContent() function is getting recomposed just because the TextField value changes 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")
        }
    }
}

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.

    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