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

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

بنية StateFlow ViewModel

تتّبع العديد من التطبيقات إرشادات تطوير التطبيقات الحديثة التي تشجّع على استخدام 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 هو بنية data متغيّرة بطبيعتها.

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