בדף הזה מופיעות דוגמאות להעברה של 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 הן מחזיקות מצב פשוטות שמגובות על ידי מערכת המצב של Compose Snapshot. הם לא שונים מהתלויות כמו 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.
גישה תואמת
שינויים כאלה בארכיטקטורה לא תמיד קלים. יכול להיות שאין לכם את החופש לבצע את השינויים האלה, או שהזמן שנדרש להשקעה עשוי להיות ארוך יותר מהיתרונות של השימוש בTextFields החדשים. במקרה כזה, עדיין אפשר להשתמש בשדות טקסט מבוססי-מצב עם שינוי קל.
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יכולה להישאר ללא שינוי.