نقل البيانات إلى الحقول النصية المستندة إلى الحالة

تقدّم هذه الصفحة أمثلة على كيفية نقل 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 عناصر بسيطة للاحتفاظ بالحالة تعتمد على نظام حالة اللقطة في 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.

النهج المتوافق

هذه الأنواع من التغييرات في البنية ليست سهلة دائمًا. قد لا يكون لديك الحرية لإجراء هذه التغييرات، أو قد يفوق الوقت المستغرق فوائد استخدام 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 كما هو.