به فیلدهای متنی مبتنی بر حالت مهاجرت کنید

این صفحه نمونه هایی از نحوه انتقال 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() جایگزین کنید.
  • با استفاده از InputTransformation منطق فیلتر را در onValueChange دوباره پیاده سازی کنید.
  • از 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 جایگزین کنید تا حداکثر طول ورودی را تنظیم کنید.
  • برای افزودن خط تیره، 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 در پست وبلاگ نوشتن توضیح داده شده است.

مشکل این است که طراحی جدید 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 شما همچنان آخرین مقادیر را از UI خواهد داشت، اما uiState: StateFlow<UiState> باعث ایجاد TextField s نخواهد شد.
  • هر منطق تداوم دیگری که در ViewModel شما پیاده‌سازی شده باشد می‌تواند ثابت بماند.