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

تقدّم هذه الصفحة أمثلة على كيفية نقل TextFieldمستندات الحالةTextField المستندة إلى القيمة إلى 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. ومع ذلك، هناك سلبيات غير متوقّعة لهذا النهج عندما يتعلّق الأمر بإدخال النص. يتم شرح مشاكل المزامنة العميقة التي تنشأ عند استخدام هذا الأسلوب بالتفصيل في مشاركة المدوّنة إدارة الحالة الفعّالة لحقل النص في 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 في العنصر القابل للإنشاء 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 إلى حاوية بسيطة للبيانات.
    • لإجراء ذلك، راقِب التغييرات التي تطرأ على كل TextFieldState.text من خلال جمع snapshotFlow في LaunchedEffect.
  • سيظلّ ViewModel يتضمّن أحدث القيم من واجهة المستخدم، ولكن لن يؤدي uiState: StateFlow<UiState> إلى تحسين TextField.
  • يمكن أن تظل أي منطق ثبات آخر تم تنفيذه في ViewModel كما هو.