انتقال به فیلدهای نوشتاری مبتنی بر وضعیت

این صفحه مثال‌هایی از نحوه انتقال 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 ذاتاً ساختار داده‌ای تغییرپذیر است.

یک توصیه رایج این است که از قرار دادن وابستگی‌های 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 در 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(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 شما پیاده‌سازی شده است می‌تواند همان‌طور باقی بماند.