בדף הזה מפורטות דוגמאות להעברה של משתני TextField
מבוססי-ערך למשתני 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) } ) }
- מחליפים את הלולאה של קריאה חוזרת (callback) עם הערך ב-
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
.
- כדי ליצור
ארכיטקטורה של StateFlow
של ViewModel
אפליקציות רבות פועלות בהתאם להנחיות לפיתוח אפליקציות מודרניות, שמעודדות שימוש ב-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
שמשתמשים בפרדיגמה של העלאת המצב (state hoisting) של 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
ב-composable של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
עדיין יופיעו הערכים האחרונים מהממשק המשתמש, אבל ה-uiState: StateFlow<UiState>
שלו לא יניב את הערכים של ה-TextField
. - כל לוגיקה אחרת של עקביות (persistence) שמיושמת ב-
ViewModel
יכולה להישאר ללא שינוי.