이 페이지에서는 값 기반 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
는 사용자 입력이 감지된 직후에 호출됩니다.InputTransformation
에서TextFieldBuffer
를 통해 제안하는 변경사항은 즉시 적용되므로 소프트웨어 키보드와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
아키텍처
많은 애플리케이션은 StateFlow
를 사용하여 모든 정보를 전달하는 단일 불변 클래스를 통해 화면 또는 구성요소의 UI 상태를 정의하는 것을 권장하는 최신 앱 개발 가이드라인을 따릅니다.
이러한 유형의 애플리케이션에서는 텍스트 입력이 있는 로그인 화면과 같은 양식이 일반적으로 다음과 같이 설계됩니다.
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
에 구현된 다른 모든 지속성 로직은 동일하게 유지할 수 있습니다.