หน้านี้แสดงตัวอย่างวิธีเปลี่ยน 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เพื่อ อัปเดตstateInputTransformationจะเรียกใช้ทันทีหลังจากตรวจพบอินพุตของผู้ใช้- การเปลี่ยนแปลงที่เสนอโดย
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เพื่อตั้งค่า ความยาวสูงสุดของอินพุต - แทนที่
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ใหม่ด้วยการเรียก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 เป็นโครงสร้างข้อมูลที่เปลี่ยนแปลงได้โดยธรรมชาติ
คำแนะนำที่พบบ่อยคือหลีกเลี่ยงการวางการอ้างอิง UI ลงใน ViewModel
แม้ว่าโดยทั่วไปแล้วนี่จะเป็นแนวทางปฏิบัติที่ดี แต่บางครั้งก็อาจตีความผิดได้
โดยเฉพาะอย่างยิ่งสำหรับทรัพยากร Dependency ของ Compose ที่เป็นโครงสร้างข้อมูลอย่างเดียวและไม่มีองค์ประกอบ 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>ด้วยค่าTextFieldState2 ค่า - ส่งออบเจ็กต์
TextFieldStateเหล่านั้นไปยังTextFieldsในLoginFormComposable
แนวทางที่สอดคล้องกัน
การเปลี่ยนแปลงด้านสถาปัตยกรรมประเภทนี้ไม่ใช่เรื่องง่ายเสมอไป คุณอาจไม่มี
อิสระในการทำการเปลี่ยนแปลงเหล่านี้ หรือการลงทุนด้านเวลาอาจมากกว่า
ประโยชน์ของการใช้ 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เป็นที่เก็บข้อมูลอย่างง่ายแทน- โดยให้สังเกตการเปลี่ยนแปลงของแต่ละ
TextFieldState.textโดย รวบรวมsnapshotFlowในLaunchedEffect
- โดยให้สังเกตการเปลี่ยนแปลงของแต่ละ
ViewModelจะยังมีค่าล่าสุดจาก UI แต่uiState: StateFlow<UiState>จะไม่ขับเคลื่อนTextField- ตรรกะความคงทนอื่นๆ ที่ใช้ใน
ViewModelจะยังคงเหมือนเดิม