이 페이지에서는 값 기반 TextField
를 상태 기반 TextField
로 이전하는 방법을 보여주는 예를 제공합니다. 값 기반 TextField
와 상태 기반 TextField
의 차이점에 대한 자세한 내용은 텍스트 필드 구성 페이지를 참고하세요.
기본 사용법
가치 기반
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
상태 기반
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
value, onValueChange
및remember { mutableStateOf("")
}를rememberTextFieldState()
로 바꿉니다.singleLine = true
를lineLimits = TextFieldLineLimits.SingleLine
로 대체했습니다.
onValueChange
을 통해 필터링
가치 기반
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
상태 기반
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- 값 콜백 루프를
rememberTextFieldState()
로 바꿉니다. InputTransformation
를 사용하여onValueChange
에서 필터링 로직을 다시 구현합니다.InputTransformation
의 수신기 범위에서TextFieldBuffer
를 사용하여state
를 업데이트합니다.InputTransformation
는 사용자 입력이 감지된 직후에 호출됩니다.TextFieldBuffer
을 통해InputTransformation
에서 제안한 변경사항은 즉시 적용되므로 소프트웨어 키보드와TextField
간의 동기화 문제가 방지됩니다.
신용카드 형식 지정기 TextField
가치 기반
@Composable fun OldTextFieldCreditCardFormatter() { var state by remember { mutableStateOf("") } TextField( value = state, onValueChange = { if (it.length <= 16) state = it }, visualTransformation = VisualTransformation { text -> // Making XXXX-XXXX-XXXX-XXXX string. var out = "" for (i in text.indices) { out += text[i] if (i % 4 == 3 && i != 15) out += "-" } TransformedText( text = AnnotatedString(out), offsetMapping = object : OffsetMapping { override fun originalToTransformed(offset: Int): Int { if (offset <= 3) return offset if (offset <= 7) return offset + 1 if (offset <= 11) return offset + 2 if (offset <= 16) return offset + 3 return 19 } override fun transformedToOriginal(offset: Int): Int { if (offset <= 4) return offset if (offset <= 9) return offset - 1 if (offset <= 14) return offset - 2 if (offset <= 19) return offset - 3 return 16 } } ) } ) }
상태 기반
@Composable fun NewTextFieldCreditCardFormatter() { val state = rememberTextFieldState() TextField( state = state, inputTransformation = InputTransformation.maxLength(16), outputTransformation = OutputTransformation { if (length > 4) insert(4, "-") if (length > 9) insert(9, "-") if (length > 14) insert(14, "-") }, ) }
onValueChange
의 필터링을InputTransformation
로 바꿔 입력의 최대 길이를 설정합니다.onValueChange
을 통한 필터링 섹션을 참고하세요.
VisualTransformation
을OutputTransformation
로 바꿔 대시를 추가합니다.VisualTransformation
를 사용하면 대시가 포함된 새 텍스트를 만들고 시각적 텍스트와 지원 상태 간에 색인이 매핑되는 방식을 계산해야 합니다.OutputTransformation
는 오프셋 매핑을 자동으로 처리합니다.OutputTransformation.transformOutput
의 수신기 범위에서TextFieldBuffer
를 사용하여 올바른 위치에 대시를 추가하기만 하면 됩니다.
상태 업데이트 (간단)
가치 기반
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
상태 기반
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- 값 콜백 루프를
rememberTextFieldState()
로 바꿉니다. TextFieldState.setTextAndPlaceCursorAtEnd
로 값 할당을 변경합니다.
상태 업데이트 (복잡)
가치 기반
@Composable fun OldTextFieldAddMarkdownEmphasis() { var markdownState by remember { mutableStateOf(TextFieldValue()) } Button(onClick = { // add ** decorations around the current selection, also preserve the selection markdownState = with(markdownState) { copy( text = buildString { append(text.take(selection.min)) append("**") append(text.substring(selection)) append("**") append(text.drop(selection.max)) }, selection = TextRange(selection.min + 2, selection.max + 2) ) } }) { Text("Bold") } TextField( value = markdownState, onValueChange = { markdownState = it }, maxLines = 10 ) }
상태 기반
@Composable fun NewTextFieldAddMarkdownEmphasis() { val markdownState = rememberTextFieldState() LaunchedEffect(Unit) { // add ** decorations around the current selection markdownState.edit { insert(originalSelection.max, "**") insert(originalSelection.min, "**") selection = TextRange(originalSelection.min + 2, originalSelection.max + 2) } } TextField( state = markdownState, lineLimits = TextFieldLineLimits.MultiLine(1, 10) ) }
이 사용 사례에서는 버튼이 마크다운 장식을 추가하여 커서 또는 현재 선택 항목 주변의 텍스트를 굵게 만듭니다. 또한 변경 후 선택 위치를 유지합니다.
- 값 콜백 루프를
rememberTextFieldState()
로 바꿉니다. maxLines = 10
를lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
로 대체했습니다.TextFieldState.edit
호출로 새TextFieldValue
를 계산하는 로직을 변경합니다.- 현재 선택에 따라 기존 텍스트를 이어 붙이고 그 사이에 마크다운 장식을 삽입하여 새
TextFieldValue
가 생성됩니다. - 또한 텍스트의 새 색인에 따라 선택 항목이 조정됩니다.
TextFieldState.edit
는TextFieldBuffer
을 사용하여 현재 상태를 수정하는 더 자연스러운 방법을 제공합니다.- 선택 영역은 장식을 삽입할 위치를 명시적으로 정의합니다.
- 그런 다음
onValueChange
접근 방식과 유사하게 선택 항목을 조정합니다.
- 현재 선택에 따라 기존 텍스트를 이어 붙이고 그 사이에 마크다운 장식을 삽입하여 새
ViewModel StateFlow
아키텍처
많은 애플리케이션이 최신 앱 개발 가이드라인을 따릅니다. 이 가이드라인에서는 모든 정보를 전달하는 단일 불변 클래스를 통해 화면이나 구성요소의 UI 상태를 정의하기 위해 StateFlow
를 사용하도록 권장합니다.
이러한 유형의 애플리케이션에서 텍스트 입력이 있는 로그인 화면과 같은 양식은 일반적으로 다음과 같이 설계됩니다.
class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> get() = _uiState.asStateFlow() fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } } data class UiState( val username: String = "", val password: String = "" ) @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { val uiState by loginViewModel.uiState.collectAsStateWithLifecycle() Column(modifier) { TextField( value = uiState.username, onValueChange = { loginViewModel.updateUsername(it) } ) TextField( value = uiState.password, onValueChange = { loginViewModel.updatePassword(it) }, visualTransformation = PasswordVisualTransformation() ) } }
이 설계는 value,
onValueChange
상태 호이스팅 패러다임을 사용하는 TextFields
에 완벽하게 적합합니다. 하지만 텍스트 입력과 관련해서는 이 접근 방식에 예측할 수 없는 단점이 있습니다. 이 접근 방식의 심층 동기화 문제는 Compose의 TextField 효과적인 상태 관리 블로그 게시물에 자세히 설명되어 있습니다.
문제는 새로운 TextFieldState
디자인이 StateFlow
지원 ViewModel UI 상태와 직접 호환되지 않는다는 점입니다. TextFieldState
은 본질적으로 변경 가능한 데이터 구조이므로 username: String
및 password: String
를 username: TextFieldState
및 password: TextFieldState
로 바꾸는 것이 이상해 보일 수 있습니다.
일반적으로 UI 종속 항목을 ViewModel
에 배치하지 않는 것이 좋습니다.
일반적으로는 좋은 방법이지만 때로는 잘못 해석될 수 있습니다.
이는 특히 순전히 데이터 구조이고 TextFieldState
과 같이 UI 요소를 포함하지 않는 Compose 종속 항목의 경우에 해당합니다.
MutableState
또는 TextFieldState
와 같은 클래스는 Compose의 스냅샷 상태 시스템으로 지원되는 간단한 상태 홀더입니다. StateFlow
또는 RxJava
와 같은 종속 항목과 다르지 않습니다. 따라서 코드에서 'ViewModel에 UI 종속 항목 없음' 원칙을 적용하는 방법을 다시 평가하는 것이 좋습니다. ViewModel
내에서 TextFieldState
에 대한 참조를 유지하는 것은 본질적으로 나쁜 방법이 아닙니다.
권장되는 간단한 접근 방식
UiState
에서 username
또는 password
와 같은 값을 추출하고 ViewModel
에 별도의 참조를 유지하는 것이 좋습니다.
class LoginViewModel : ViewModel() { val usernameState = TextFieldState() val passwordState = TextFieldState() } @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { Column(modifier) { TextField(state = loginViewModel.usernameState,) SecureTextField(state = loginViewModel.passwordState) } }
MutableStateFlow<UiState>
를 몇 개의TextFieldState
값으로 바꿉니다.- 이러한
TextFieldState
객체를LoginForm
컴포저블의TextFields
에 전달합니다.
준수 접근 방식
이러한 유형의 아키텍처 변경은 쉽지 않은 경우가 많습니다. 이러한 변경사항을 적용할 수 없거나 시간 투자에 비해 새 TextField
사용의 이점이 적을 수 있습니다. 이 경우 약간의 수정으로 상태 기반 텍스트 필드를 계속 사용할 수 있습니다.
class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> get() = _uiState.asStateFlow() fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } } data class UiState( val username: String = "", val password: String = "" ) @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { val initialUiState = remember(loginViewModel) { loginViewModel.uiState.value } Column(modifier) { val usernameState = rememberTextFieldState(initialUiState.username) LaunchedEffect(usernameState) { snapshotFlow { usernameState.text.toString() }.collectLatest { loginViewModel.updateUsername(it) } } TextField(usernameState) val passwordState = rememberTextFieldState(initialUiState.password) LaunchedEffect(usernameState) { snapshotFlow { usernameState.text.toString() }.collectLatest { loginViewModel.updatePassword(it) } } SecureTextField(passwordState) } }
ViewModel
및UiState
클래스를 동일하게 유지합니다.- 상태를
ViewModel
에 직접 호이스팅하고TextFields
의 소스 저장소로 만드는 대신ViewModel
을 간단한 데이터 홀더로 전환합니다.- 이렇게 하려면
LaunchedEffect
에서snapshotFlow
를 수집하여 각TextFieldState.text
의 변경사항을 관찰합니다.
- 이렇게 하려면
ViewModel
에는 UI의 최신 값이 계속 표시되지만uiState: StateFlow<UiState>
가TextField
를 제어하지는 않습니다.ViewModel
에 구현된 다른 지속성 로직은 동일하게 유지될 수 있습니다.