בדף הזה מופיעות דוגמאות להעברה של TextField
מבוססי-ערך לTextField
מבוססי-מצב. במאמר הגדרת שדות טקסט יש מידע על ההבדלים בין TextField
s מבוססי ערך לבין TextField
s מבוססי מצב.
שימוש בסיסי
מבוסס-ערך
@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
ב-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
ולהפוך אותו למקור האמת שלTextFields
, הופכים אתViewModel
למאגר נתונים פשוט.- כדי לעשות את זה, צריך לעקוב אחרי השינויים בכל
TextFieldState.text
על ידי איסוףsnapshotFlow
בLaunchedEffect
.
- כדי לעשות את זה, צריך לעקוב אחרי השינויים בכל
- ב-
ViewModel
עדיין יוצגו הערכים האחרונים מממשק המשתמש, אבלuiState: StateFlow<UiState>
לא יניב אתTextField
. - כל לוגיקה אחרת של שמירת נתונים שהוטמעה ב-
ViewModel
יכולה להישאר ללא שינוי.