หน้านี้แสดงตัวอย่างวิธีเปลี่ยน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
เพื่อกำหนด ความยาวสูงสุดของอินพุต - แทนที่
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
ใหม่โดยการตัดต่อข้อความที่มีอยู่ตาม ข้อความที่เลือกในปัจจุบัน และแทรกการตกแต่งมาร์กดาวน์ไว้ตรงกลาง - นอกจากนี้ ระบบจะปรับการเลือกตามดัชนีใหม่ของข้อความด้วย
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>
ด้วยค่าTextFieldState
2 ค่า - ส่งออบเจ็กต์
TextFieldState
เหล่านั้นไปยังTextFields
ในLoginForm
ที่ประกอบได้
แนวทางที่สอดคล้องกัน
การเปลี่ยนแปลงด้านสถาปัตยกรรมประเภทนี้ไม่ใช่เรื่องง่ายเสมอไป คุณอาจไม่มี
อิสระในการทำการเปลี่ยนแปลงเหล่านี้ หรือการลงทุนด้านเวลาอาจมากกว่า
ประโยชน์ของการใช้ 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
จะยังคงเหมือนเดิม