หน้านี้แสดงตัวอย่างวิธีย้ายข้อมูล 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()
- ใช้ตรรกะการกรองใน
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) }
- แทนที่ลูปการเรียกกลับค่าด้วย
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)
- เปลี่ยนตรรกะการคํานวณ
TextFieldValue
ใหม่ด้วย CallTypeTextFieldState.edit
- ระบบจะสร้าง
TextFieldValue
ใหม่โดยการต่อข้อความที่มีอยู่ตามการเลือกปัจจุบัน และแทรกการตกแต่ง Markdown ไว้ตรงกลาง - นอกจากนี้ ระบบจะปรับการเลือกตามดัชนีใหม่ของข้อความด้วย
TextFieldState.edit
มีวิธีแก้ไขสถานะปัจจุบันที่เป็นธรรมชาติมากขึ้นเมื่อใช้TextFieldBuffer
- การเลือกจะกำหนดตำแหน่งที่จะแทรกการตกแต่งอย่างชัดเจน
- จากนั้นปรับการเลือกให้คล้ายกับแนวทาง
onValueChange
- ระบบจะสร้าง
สถาปัตยกรรม StateFlow
ของ ViewModel
แอปพลิเคชันจำนวนมากเป็นไปตามหลักเกณฑ์การพัฒนาแอปสมัยใหม่ ซึ่งส่งเสริมให้ใช้ 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
เป็นโครงสร้างข้อมูลที่เปลี่ยนแปลงได้อยู่แล้ว
คําแนะนําทั่วไปคือหลีกเลี่ยงการใส่ทรัพยากร ViewModel
ไว้ใน ViewModel
แม้ว่าโดยทั่วไปแล้วแนวทางนี้จะเป็นสิ่งที่ควรทำ แต่บางครั้งก็อาจมีการตีความที่ผิด
โดยเฉพาะอย่างยิ่งสำหรับ Compose Dependencies ที่เป็นโครงสร้างข้อมูลล้วนๆ และไม่มีองค์ประกอบ UI เช่น TextFieldState
คลาสอย่าง MutableState
หรือ TextFieldState
เป็นตัวเก็บสถานะแบบง่ายที่รองรับโดยระบบสถานะภาพรวมของ Compose ซึ่งไม่แตกต่างจากข้อกำหนดเบื้องต้น เช่น StateFlow
หรือ RxJava
เราจึงขอแนะนำให้คุณประเมินอีกครั้งว่าคุณใช้หลักการ "ไม่มีทรัพยากร 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>
ด้วยค่าTextFieldState
2 ค่า - ส่งออบเจ็กต์
TextFieldState
เหล่านั้นไปยังTextFields
ในLoginForm
composable
แนวทางการปฏิบัติตามข้อกำหนด
การเปลี่ยนแปลงสถาปัตยกรรมประเภทนี้ไม่ใช่เรื่องง่ายเสมอไป คุณอาจไม่มีอิสระที่จะทำการเปลี่ยนแปลงเหล่านี้ หรือการลงทุนด้านเวลาอาจไม่คุ้มค่ากับประโยชน์ที่ได้รับจากการใช้ 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
โดยตรงและทำให้ViewModel
เป็นแหล่งข้อมูลที่เป็นความจริงสำหรับTextFields
ให้เปลี่ยนViewModel
เป็นผู้เก็บข้อมูลธรรมดา- โดยสังเกตการเปลี่ยนแปลงของ
TextFieldState.text
แต่ละรายการด้วยการรวบรวมsnapshotFlow
ในLaunchedEffect
- โดยสังเกตการเปลี่ยนแปลงของ
ViewModel
จะยังคงมีค่าล่าสุดจาก UI แต่uiState: StateFlow<UiState>
ของViewModel
จะไม่เป็นตัวขับเคลื่อนTextField
- ตรรกะการคงข้อมูลอื่นๆ ที่ใช้กับ
ViewModel
จะยังคงเหมือนเดิม