این صفحه مثالهایی از نحوه انتقال 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جایگزین کنید تا حداکثر طول ورودی را تنظیم کنید.- به بخش فیلتر کردن ازطریق
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 ذاتاً ساختار دادهای
تغییرپذیر است.
یک توصیه رایج این است که از قرار دادن وابستگیهای UI در 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(passwordState) { snapshotFlow { passwordState.text.toString() }.collectLatest { loginViewModel.updatePassword(it) } } SecureTextField(passwordState) } }
- کلاسهای
ViewModelوUiStateرا یکسان نگه دارید. - بهجای اینکه وضعیت را مستقیماً به
ViewModelمنتقل کنید و آن را به منبع حقیقت برایTextFieldsتبدیل کنید،ViewModelرا به یک نگهدارنده داده ساده تبدیل کنید.- برای انجام این کار، تغییرات هر
TextFieldState.textرا با جمعآوریsnapshotFlowدرLaunchedEffectمشاهده کنید.
- برای انجام این کار، تغییرات هر
ViewModelشما همچنان جدیدترین مقادیر را از واسط کاربر خواهد داشت، اماuiState: StateFlow<UiState>آنTextFieldرا هدایت نخواهد کرد.- هر منطق پایداری دیگری که در
ViewModelشما پیادهسازی شده است میتواند همانطور باقی بماند.