בדף הזה מפורטות דוגמאות להעברה של TextFieldמבוססי-ערך לTextFieldמבוססי-מצב. במאמר הגדרת שדות טקסט מוסבר על ההבדלים בין TextFields מבוססי ערך לבין TextFields מבוססי מצב.
שימוש בסיסי
מבוסס-ערך
@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) ) }
בתרחיש השימוש הזה, לחיצה על לחצן מוסיפה את סימני העיצוב של Markdown כדי להפוך את הטקסט לטקסט מודגש סביב הסמן או הבחירה הנוכחית. הוא גם שומר על מיקום הבחירה אחרי השינויים.
- מחליפים את לולאת הקריאה החוזרת של הערך ב-
rememberTextFieldState(). - מחליפים את
maxLines = 10ב-lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - שינוי הלוגיקה של חישוב
TextFieldValueחדש באמצעות קריאה ל-TextFieldState.edit.- נוצרת
TextFieldValueחדשה על ידי פיצול הטקסט הקיים על סמך הבחירה הנוכחית, והוספת עיטורי Markdown באמצע. - בנוסף, הבחירה מותאמת בהתאם לאינדקסים החדשים של הטקסט.
- ל-
TextFieldState.editיש דרך טבעית יותר לערוך את המצב הנוכחי באמצעותTextFieldBuffer. - הבחירה מגדירה באופן מפורש איפה להוסיף את הקישוטים.
- לאחר מכן, משנים את החלק שנבחר, בדומה לשיטה של
onValueChange.
- נוצרת
ארכיטקטורת ViewModel StateFlow
הרבה אפליקציות פועלות לפי ההנחיות לפיתוח אפליקציות מודרניות, שמקדמות שימוש ב-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. עם זאת, יש חסרונות בלתי צפויים לגישה הזו כשמדובר בהזנת טקסט. בפוסט בבלוג בנושא ניהול יעיל של מצב TextField ב-Compose מוסבר בפירוט על בעיות הסנכרון העמוק בגישה הזו.
הבעיה היא שהעיצוב החדש של TextFieldState לא תואם ישירות למצב ממשק המשתמש של ViewModel שמגובה על ידי StateFlow. יכול להיות שייראה לכם מוזר להחליף את username: String ואת password: String ב-username: TextFieldState וב-password: TextFieldState, כי TextFieldState הוא מבנה נתונים שניתן לשינוי.
המלצה נפוצה היא להימנע מהצבת תלות בממשק משתמש ב-ViewModel.
למרות שבדרך כלל מומלץ לעשות את זה, לפעמים אפשר לפרש את זה לא נכון.
זה נכון במיוחד לגבי תלות ב-Compose שהן מבני נתונים בלבד ולא כוללות רכיבי ממשק משתמש, כמו TextFieldState.
מחלקות כמו MutableState או TextFieldState הן מחזיקות מצב פשוטות שמגובות על ידי מערכת המצב של Snapshot ב-Compose. הם לא שונים מהתלויות StateFlow או RxJava. לכן,מומלץ לבדוק מחדש את האופן שבו אתם מיישמים את העיקרון 'אין תלות בממשק המשתמש ב-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. - מעבירים את האובייקטים האלה
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עדיין יוצגו הערכים האחרונים מממשק המשתמש, אבלuiState: StateFlow<UiState>לא יניב אתTextField. - כל לוגיקה אחרת של שמירת נתונים שהוטמעה ב-
ViewModelיכולה להישאר ללא שינוי.