이 페이지에서는 가치 기반 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() ) } }
이 설계는 TextFields 상태 호이스팅 패러다임을 사용하는 value,
onValueChange에 완벽하게 적합합니다. 하지만 텍스트 입력과 관련하여 이 접근 방식에는 예측할 수 없는 단점이 있습니다. 이 접근 방식의 심층 동기화
문제는 Compose의 TextField를 위한 효과적인 상태
관리 블로그 게시물에서 자세히 설명합니다.
문제는 새 TextFieldState 설계가 StateFlow 지원 ViewModel UI 상태와 직접 호환되지 않는다는 것입니다. TextFieldState는 본질적으로 변경 가능한 데이터
구조이므로
username: String 및 password: String을 username: TextFieldState 및
password: TextFieldState로 바꾸는 것이 이상해 보일 수 있습니다.
일반적으로 ViewModel에 UI 종속 항목을 배치하지 않는 것이 좋습니다.
일반적으로 좋은 방법이지만 때로는 오해될 수 있습니다.
특히 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값으로 바꿉니다.LoginForm컴포저블의TextFields에TextFieldState객체를 전달합니다.
준수 접근 방식
이러한 유형의 아키텍처 변경은 항상 쉬운 것은 아닙니다. 이러한 변경을 자유롭게 할 수 없거나 시간 투자가 새 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에 구현된 다른 지속성 로직은 동일하게 유지할 수 있습니다.