Избегайте перекомпоновки других элементов при изменении значения TextField
У меня есть основной экран входа в приложение, который выглядит так:
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
)
}
И я использую его с моделью просмотра, чтобы решить, следует ли включить кнопку входа в систему, войти в систему пользователя и т. д. 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
}
}
}
}
}
Однако проблема в том, что когда я печатаю что-то в TextFields в инспекторе компоновки, я вижу, что другие элементы пользовательского интерфейса (например, Button и ClickableButton) перекомпоновываются:
Как избежать такого поведения?
1 ответ
@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
)
}