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

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