این صفحه مثالهایی از نحوهی انتقال 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 یک ساختار داده ذاتاً تغییرپذیر است.
یک توصیه رایج این است که از قرار دادن وابستگیهای رابط کاربری در 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در ComposableLoginFormمنتقل کنید.
رویکرد منطبق
این نوع تغییرات معماری همیشه آسان نیستند. ممکن است شما آزادی عمل لازم برای اعمال این تغییرات را نداشته باشید، یا زمان صرف شده برای آن از مزایای استفاده از 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به یک نگهدارنده داده ساده تبدیل کنید.- برای انجام این کار، با جمعآوری یک
snapshotFlowدرLaunchedEffect، تغییرات هرTextFieldState.textرا مشاهده کنید.
- برای انجام این کار، با جمعآوری یک
-
ViewModelشما همچنان آخرین مقادیر UI را خواهد داشت، اماuiState: StateFlow<UiState>آن،TextFieldها را هدایت نخواهد کرد. - هر منطق پایداری دیگری که در
ViewModelشما پیادهسازی شده است، میتواند به همان شکل باقی بماند.