หน้านี้แสดงตัวอย่างวิธีที่คุณสามารถย้ายข้อมูล 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) } ) }
- แทนที่ลูป Callback ของค่าด้วย
rememberTextFieldState() - ใช้ตรรกะการกรองอีกครั้งใน
onValueChangeโดยใช้InputTransformation - ใช้
TextFieldBufferจากขอบเขตตัวรับสัญญาณของInputTransformationเพื่ออัปเดต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จะจัดการการแมปออฟเซ็ตโดยอัตโนมัติ คุณเพียงแค่ต้องเพิ่มขีดกลางในตำแหน่งที่ถูกต้องโดยใช้TextFieldBufferจากขอบเขตตัวรับสัญญาณของOutputTransformation.transformOutput
- เมื่อใช้
การอัปเดตสถานะ (แบบง่าย)
ตามมูลค่า
@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) }
- แทนที่ลูป Callback ของค่าด้วย
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) ) }
ในกรณีการใช้งานนี้ ปุ่มจะเพิ่มการตกแต่งมาร์กดาวน์เพื่อทำให้ข้อความรอบเคอร์เซอร์หรือส่วนที่เลือกในปัจจุบันเป็นตัวหนา นอกจากนี้ยังคงตำแหน่งการเลือกไว้หลังจากการเปลี่ยนแปลงด้วย
- แทนที่ลูป Callback ของค่าด้วย
rememberTextFieldState() - แทนที่
maxLines = 10ด้วยlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10) - เปลี่ยนตรรกะการคำนวณ
TextFieldValueใหม่ด้วยการเรียกTextFieldState.edit- ระบบจะสร้าง
TextFieldValueใหม่โดยการแยกข้อความที่มีอยู่ตามการเลือกปัจจุบัน และแทรกการตกแต่ง Markdown ไว้ระหว่างนั้น - นอกจากนี้ ระบบจะปรับการเลือกตามดัชนีใหม่ของข้อความด้วย
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() ) } }
การออกแบบนี้เหมาะอย่างยิ่งกับ TextFields ที่ใช้กระบวนทัศน์การย้ายสถานะ value,
onValueChange อย่างไรก็ตาม วิธีการนี้มีข้อเสียที่คาดเดาไม่ได้เมื่อพูดถึงการป้อนข้อความ ปัญหาการซิงค์ข้อมูลเชิงลึกเกี่ยวกับแนวทางนี้จะอธิบายไว้อย่างละเอียดในบล็อกโพสต์การจัดการสถานะที่มีประสิทธิภาพสำหรับ TextField ใน Compose
ปัญหาคือการออกแบบ TextFieldState ใหม่ไม่เข้ากันโดยตรงกับสถานะ UI ของ ViewModel ที่สนับสนุนโดย StateFlow การแทนที่ username: String และ password: String ด้วย username: TextFieldState และ password: TextFieldState อาจดูแปลก เนื่องจาก TextFieldState เป็นโครงสร้างข้อมูลที่เปลี่ยนแปลงได้โดยเนื้อแท้
คำแนะนำที่พบบ่อยคือให้หลีกเลี่ยงการวางทรัพยากร Dependency ของ UI ไว้ใน ViewModel
แม้ว่าโดยทั่วไปแล้วการดำเนินการนี้จะเป็นแนวทางปฏิบัติที่ดี แต่บางครั้งก็อาจมีการตีความผิดได้
โดยเฉพาะอย่างยิ่งสำหรับทรัพยากร Dependency ของ Compose ที่เป็นโครงสร้างข้อมูลล้วนๆ และไม่มีองค์ประกอบ UI ใดๆ เช่น TextFieldState
คลาสอย่าง MutableState หรือ TextFieldState เป็นตัวเก็บสถานะแบบง่ายที่ได้รับการสนับสนุนโดยระบบสถานะ Snapshot ของ Compose ซึ่งไม่ต่างจากทรัพยากร Dependency อย่าง StateFlow หรือ RxJava ดังนั้น เราขอแนะนำให้คุณประเมินอีกครั้งว่าคุณใช้หลักการ "ไม่มีทรัพยากร Dependency ของ UI ใน ViewModel" ในโค้ดอย่างไร การเก็บข้อมูลอ้างอิงถึง TextFieldState ไว้ใน ViewModel ไม่ใช่แนวทางปฏิบัติที่ไม่ดีโดยเนื้อแท้
แนวทางง่ายๆ ที่แนะนำ
เราขอแนะนำให้ดึงค่าต่างๆ เช่น username หรือ password ออกจาก UiState และเก็บข้อมูลอ้างอิงแยกต่างหากไว้ใน 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>ด้วยค่าTextFieldState2 ค่า - ส่งออบเจ็กต์
TextFieldStateเหล่านั้นไปยังTextFieldsใน ComposableLoginForm
แนวทางที่สอดคล้องกัน
การเปลี่ยนแปลงสถาปัตยกรรมประเภทนี้ไม่ใช่เรื่องง่ายเสมอไป คุณอาจไม่มีอิสระในการเปลี่ยนแปลงเหล่านี้ หรือเวลาที่ต้องใช้ในการดำเนินการอาจไม่คุ้มค่ากับประโยชน์ที่ได้จากการใช้ 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(passwordState) { snapshotFlow { passwordState.text.toString() }.collectLatest { loginViewModel.updatePassword(it) } } SecureTextField(passwordState) } }
- คงชั้นเรียน
ViewModelและUiStateไว้เหมือนเดิม - แทนที่จะยกสถานะขึ้นเป็น
ViewModelโดยตรงและทำให้เป็นแหล่งข้อมูลที่เชื่อถือได้สำหรับTextFieldsให้เปลี่ยนViewModelเป็นตัวเก็บข้อมูลธรรมดา- โดยสังเกตการเปลี่ยนแปลงของ
TextFieldState.textแต่ละรายการด้วยการรวบรวมsnapshotFlowในLaunchedEffect
- โดยสังเกตการเปลี่ยนแปลงของ
ViewModelจะยังคงมีค่าล่าสุดจาก UI แต่uiState: StateFlow<UiState>จะไม่ขับเคลื่อนTextField- ตรรกะความต่อเนื่องอื่นๆ ที่ใช้ใน
ViewModelจะยังคงเหมือนเดิม