Avoid recompositions of other elements when changing TextField value

494 Views Asked by At

I have basic Login screen in my app, which looks like this:

LoginScreen.kt:

@Composable
fun LoginScreen(navController: NavController, viewModel: LoginViewModel) {
    val userEmail = viewModel.userEmail.collectAsState()
    val userPassword = viewModel.userPassword.collectAsState()
    val isLoginPending = viewModel.isLoginPending.collectAsState()
    val isLoginButtonEnabled = viewModel.isLoginButtonEnabled.collectAsState()

    val scaffoldState = rememberScaffoldState()

    if (viewModel.getOnboardingStatus() == OnboardingStatus.NOT_COMPLETED) {
        navController.navigate(Screen.Onboarding.route)
    }

    LaunchedEffect(key1 = true) {
        viewModel.loginError.collectLatest {
            it?.let {
                scaffoldState.snackbarHostState.showSnackbar(message = it)
            }
        }
    }

    Scaffold(
        modifier = Modifier
            .fillMaxSize()
            .recomposeHighlighter(),
        scaffoldState = scaffoldState
    ) {
        Box(modifier = Modifier
            .fillMaxSize()
            .recomposeHighlighter()) {
            if (isLoginPending.value) {
                ProgressBar(
                    modifier = Modifier
                        .align(Alignment.Center)
                        .recomposeHighlighter(),
                )
            }
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .align(Alignment.Center)
                    .recomposeHighlighter(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text(
                    text = "Login",
                    fontWeight = FontWeight.Bold,
                    fontSize = 36.sp
                )
                Spacer(modifier = Modifier
                    .height(64.dp)
                    .recomposeHighlighter())
                UserInputTextField(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(start = 16.dp, end = 16.dp)
                        .recomposeHighlighter(),
                    label = "Email",
                    inputState = userEmail.value,
                    onValueChange = { viewModel.onEvent(LoginEvent.EnteredEmail(it)) },
                    isLoginPending = isLoginPending.value
                )
                Spacer(modifier = Modifier
                    .height(8.dp)
                    .recomposeHighlighter())
                UserInputTextField(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(start = 16.dp, end = 16.dp)
                        .recomposeHighlighter(),
                    label = "Password",
                    inputState = userPassword.value,
                    onValueChange = { viewModel.onEvent(LoginEvent.EnteredPassword(it)) },
                    isLoginPending = isLoginPending.value,
                    keyboard = KeyboardOptions(keyboardType = KeyboardType.Password),
                    visualTransformation = PasswordVisualTransformation()
                )
                Spacer(modifier = Modifier
                    .height(32.dp)
                    .recomposeHighlighter())
                Button(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(
                            start = 16.dp,
                            end = 16.dp
                        )
                        .recomposeHighlighter(),
                    enabled = isLoginButtonEnabled.value,
                    onClick = {
                        viewModel.onEvent(LoginEvent.Login)
                    }
                ) {
                    Text(text = "Login")
                }
            }
            ClickableButton(
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(bottom = 32.dp)
                    .recomposeHighlighter(),
                text = "No account? Click to register",
                onTextClicked = { navController.navigate(Screen.Register.route) }
            )
        }
    }
}

@Composable
private fun UserInputTextField(
    modifier: Modifier,
    label: String,
    inputState: String,
    onValueChange: (String) -> Unit,
    isLoginPending: Boolean,
    keyboard: KeyboardOptions? = null,
    visualTransformation: VisualTransformation? = null
) {
    OutlinedTextField(
        value = inputState,
        onValueChange = { onValueChange(it) },
        label = { Text(text = label) },
        placeholder = { Text(text = label) },
        singleLine = true,
        enabled = !isLoginPending,
        modifier = modifier,
        keyboardOptions = keyboard?.let { keyboard } ?: KeyboardOptions.Default,
        visualTransformation = visualTransformation?.let { visualTransformation } ?: VisualTransformation.None
    )
}

And I am using it with view model to decide if login button should be enabled, to login user etc. LoginViewModel:

@HiltViewModel
class LoginViewModel @Inject constructor(private val repository: LoginRepository): ViewModel() {

    private val _userEmail = MutableStateFlow<String>(value = String())
    val userEmail: StateFlow<String> = _userEmail.asStateFlow()

    private val _userPassword = MutableStateFlow<String>(value = String())
    val userPassword: StateFlow<String> = _userPassword.asStateFlow()

    private val _isLoginPending = MutableStateFlow<Boolean>(value = false)
    val isLoginPending: StateFlow<Boolean> = _isLoginPending.asStateFlow()

    private val _loginError = MutableSharedFlow<String?>()
    val loginError: SharedFlow<String?> = _loginError.asSharedFlow()

    private val _isLoginButtonEnabled = MutableStateFlow<Boolean>(value = false)
    val isLoginButtonEnabled: StateFlow<Boolean> = _isLoginButtonEnabled.asStateFlow()

    fun getOnboardingStatus(): OnboardingStatus {
        return repository.isOnboardingCompleted()
    }

    fun onEvent(event: LoginEvent) {
        when (event) {
            is LoginEvent.EnteredEmail -> {
                _userEmail.value = event.email
                _isLoginButtonEnabled.value = _userEmail.value.isNotEmpty() && _userPassword.value.isNotEmpty()
            }
            is LoginEvent.EnteredPassword -> {
                _userPassword.value = event.password
                _isLoginButtonEnabled.value = _userEmail.value.isNotEmpty() && _userPassword.value.isNotEmpty()
            }
            is LoginEvent.Login -> {
                viewModelScope.launch {
                    _isLoginPending.value = true
                    _isLoginButtonEnabled.value = false

                    val result = repository.loginUser(_userEmail.value, _userPassword.value)
                    when (result) {
                        is AuthRequestState.Success -> { /* nothing to do */ }
                        is AuthRequestState.Fail -> {
                            _loginError.emit(result.msg)
                        }
                    }

                    _isLoginPending.value = false
                    _isLoginButtonEnabled.value = true
                }
            }
        }
    }

}

However the problem is that when I am typing something in TextFields in layout inspector I see that other elements of UI (like Button and ClickableButton) are recomposed: enter image description here

How to avoid such behavior?

1

There are 1 best solutions below

2
On
@Composable
fun LoginScreen(navController: NavController, viewModel: LoginViewModel) {
    val userEmail = viewModel.userEmail.collectAsState()
    val userPassword = viewModel.userPassword.collectAsState()
    val isLoginPending = viewModel.isLoginPending.collectAsState()
    val isLoginButtonEnabled = viewModel.isLoginButtonEnabled.collectAsState()

    // this line solved the recomposition problems for me.
    // replace wherever viewModel.onEvent with  onEvent 
    val onEvent:(LoginEvent) -> Unit = viewModel::onEvent

    val scaffoldState = rememberScaffoldState()

    if (viewModel.getOnboardingStatus() == OnboardingStatus.NOT_COMPLETED) {
        navController.navigate(Screen.Onboarding.route)
    }

    LaunchedEffect(key1 = true) {
        viewModel.loginError.collectLatest {
            it?.let {
                scaffoldState.snackbarHostState.showSnackbar(message = it)
            }
        }
    }

    Scaffold(
        modifier = Modifier
            .fillMaxSize()
            .recomposeHighlighter(),
        scaffoldState = scaffoldState
    ) {
        Box(modifier = Modifier
            .fillMaxSize()
            .recomposeHighlighter()) {
            if (isLoginPending.value) {
                ProgressBar(
                    modifier = Modifier
                        .align(Alignment.Center)
                        .recomposeHighlighter(),
                )
            }
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .align(Alignment.Center)
                    .recomposeHighlighter(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text(
                    text = "Login",
                    fontWeight = FontWeight.Bold,
                    fontSize = 36.sp
                )
                Spacer(modifier = Modifier
                    .height(64.dp)
                    .recomposeHighlighter())
                UserInputTextField(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(start = 16.dp, end = 16.dp)
                        .recomposeHighlighter(),
                    label = "Email",
                    inputState = userEmail.value,
                    onValueChange = { onEvent(LoginEvent.EnteredEmail(it)) },
                    isLoginPending = isLoginPending.value
                )
                Spacer(modifier = Modifier
                    .height(8.dp)
                    .recomposeHighlighter())
                UserInputTextField(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(start = 16.dp, end = 16.dp)
                        .recomposeHighlighter(),
                    label = "Password",
                    inputState = userPassword.value,
                    onValueChange = { onEvent(LoginEvent.EnteredPassword(it)) },
                    isLoginPending = isLoginPending.value,
                    keyboard = KeyboardOptions(keyboardType = KeyboardType.Password),
                    visualTransformation = PasswordVisualTransformation()
                )
                Spacer(modifier = Modifier
                    .height(32.dp)
                    .recomposeHighlighter())
                Button(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(
                            start = 16.dp,
                            end = 16.dp
                        )
                        .recomposeHighlighter(),
                    enabled = isLoginButtonEnabled.value,
                    onClick = {
                        viewModel.onEvent(LoginEvent.Login)
                    }
                ) {
                    Text(text = "Login")
                }
            }
            ClickableButton(
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(bottom = 32.dp)
                    .recomposeHighlighter(),
                text = "No account? Click to register",
                onTextClicked = { navController.navigate(Screen.Register.route) }
            )
        }
    }
}

@Composable
private fun UserInputTextField(
    modifier: Modifier,
    label: String,
    inputState: String,
    onValueChange: (String) -> Unit,
    isLoginPending: Boolean,
    keyboard: KeyboardOptions? = null,
    visualTransformation: VisualTransformation? = null
) {
    OutlinedTextField(
        value = inputState,
        onValueChange = { onValueChange(it) },
        label = { Text(text = label) },
        placeholder = { Text(text = label) },
        singleLine = true,
        enabled = !isLoginPending,
        modifier = modifier,
        keyboardOptions = keyboard?.let { keyboard } ?: KeyboardOptions.Default,
        visualTransformation = visualTransformation?.let { visualTransformation } ?: VisualTransformation.None
    )
}