স্টেট-ভিত্তিক টেক্সট ফিল্ডে মাইগ্রেট করা

এই পৃষ্ঠায় আপনি ভ্যালু-ভিত্তিক 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() ব্যবহার করুন।
  • InputTransformation ব্যবহার করে onValueChange-এ ফিল্টারিং লজিক আবার প্রয়োগ করুন।
  • state আপডেট করতে InputTransformation-এর রিসিভার স্কোপ থেকে TextFieldBuffer ব্যবহার করুন।
    • ব্যবহারকারীর ইনপুট শনাক্ত করার ঠিক পরেই InputTransformation কল করা হয়।
    • TextFieldBuffer-এর মাধ্যমে InputTransformation-এর প্রস্তাবিত পরিবর্তনগুলি অবিলম্বে প্রয়োগ করা হয়, যা সফ্টওয়্যার কীবোর্ড এবং 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 অটোমেটিক অফসেট ম্যাপিংয়ের যত্ন নেয়। আপনাকে শুধু OutputTransformation.transformOutput-এর রিসিভার স্কোপ থেকে TextFieldBuffer ব্যবহার করে সঠিক জায়গায় ড্যাশ যোগ করতে হবে।

স্ট্যাটাস আপডেট করা হচ্ছে (সহজ)

ভ্যালু-ভিত্তিক

@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)
    )
}

এই ব্যবহারের ক্ষেত্রে, একটি বোতাম কার্সরের চারপাশে বা বর্তমান নির্বাচনের চারপাশে টেক্সট বোল্ড করতে মার্কডাউন ডেকোরেশন যোগ করে। এটি পরিবর্তন করার পরেও বেছে নেওয়ার পজিশন বজায় রাখে।

  • ভ্যালু কলব্যাক লুপের পরিবর্তে rememberTextFieldState() ব্যবহার করুন।
  • maxLines = 10-এর জায়গায় lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10) ব্যবহার করুন।
  • TextFieldState.edit কল ব্যবহার করে নতুন TextFieldValue গণনা করার লজিক পরিবর্তন করুন।
    • বর্তমান বেছে নেওয়া বিকল্পের উপর ভিত্তি করে আগে থেকে থাকা টেক্সট স্প্লাইস করে এবং মাঝে Markdown ডেকোরেশন যোগ করে নতুন TextFieldValue তৈরি করা হয়।
    • এছাড়াও, টেক্সটের নতুন ইন্ডেক্স অনুযায়ী বেছে নেওয়া অংশ অ্যাডজাস্ট করা হয়।
    • TextFieldBuffer ব্যবহার করে বর্তমান অবস্থা এডিট করার আরও স্বাভাবিক উপায় TextFieldState.edit-এর কাছে আছে।
    • কোথায় সাজসজ্জা যোগ করতে হবে তা বেছে নেওয়া বিকল্পটি স্পষ্টভাবে উল্লেখ করে।
    • তারপর, onValueChange পদ্ধতির মতো, বেছে নেওয়া অংশ অ্যাডজাস্ট করুন।

ViewModel StateFlow আর্কিটেকচার

অনেক অ্যাপ্লিকেশন আধুনিক অ্যাপ ডেভেলপমেন্ট নির্দেশিকা অনুসরণ করে, যা একটি স্ক্রিন বা উপাদানের UI স্টেটকে সংজ্ঞায়িত করার জন্য একটি 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-এ TextField-এর জন্য কার্যকর স্টেট ম্যানেজমেন্ট ব্লগ পোস্টে বিস্তারিতভাবে ব্যাখ্যা করা হয়েছে।

সমস্যা হল, নতুন TextFieldState ডিজাইন সরাসরি StateFlow ব্যাকড ViewModel UI স্টেটের সাথে মানানসই নয়। username: String এবং password: String-এর পরিবর্তে username: TextFieldState এবং password: TextFieldState ব্যবহার করা অদ্ভুত লাগতে পারে, কারণ TextFieldState হল সহজাতভাবে পরিবর্তনশীল ডেটা স্ট্রাকচার।

সাধারণত, ViewModel-এ UI ডিপেন্ডেন্সি না রাখার সাজেশন দেওয়া হয়। যদিও এটি সাধারণত একটি ভাল অভ্যাস, তবে কখনও কখনও এটি ভুলভাবে ব্যাখ্যা করা হতে পারে। এটি বিশেষ করে Compose ডিপেন্ডেন্সির ক্ষেত্রে প্রযোজ্য যা সম্পূর্ণভাবে ডেটা স্ট্রাকচার এবং এর সাথে কোনও UI এলিমেন্ট থাকে না, যেমন TextFieldState।

MutableState বা TextFieldState-এর মতো ক্লাস হল সাধারণ স্টেট হোল্ডার যা Compose-এর স্ন্যাপশট স্টেট সিস্টেম দ্বারা ব্যাক-আপ করা হয়। এগুলি StateFlow বা RxJava-এর মতো ডিপেন্ডেন্সির থেকে আলাদা নয়। তাই,আপনার কোডে "ViewModel-এ কোনও UI ডিপেন্ডেন্সি নেই" নীতিটি কীভাবে প্রয়োগ করবেন তা পুনরায় মূল্যায়ন করার জন্য আমরা আপনাকে উৎসাহিত করি। আপনার ViewModel-এর মধ্যে TextFieldState-এর রেফারেন্স রাখা খারাপ অভ্যাস নয়।

আমরা সাজেস্ট করি যে আপনি UiState থেকে username বা password-এর মতো ভ্যালু এক্সট্র্যাক্ট করুন, এবং 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 ভ্যালু ব্যবহার করুন।
  • LoginForm কম্পোজেবলে TextFields-এ সেই TextFieldState অবজেক্টগুলি পাস করুন।

সম্মতিসূচক পদ্ধতি

এই ধরনের আর্কিটেকচারাল পরিবর্তন করা সবসময় সহজ হয় না। এইসব পরিবর্তন করার স্বাধীনতা আপনার নাও থাকতে পারে অথবা নতুন 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-কে একটি সাধারণ ডেটা হোল্ডারে পরিণত করুন।
    • এটি করতে, LaunchedEffect-এ snapshotFlow সংগ্রহ করে প্রতিটি TextFieldState.text-এ হওয়া পরিবর্তন লক্ষ্য করুন।
  • আপনার ViewModel-এ এখনও UI থেকে লেটেস্ট ভ্যালু থাকবে, তবে এর uiState: StateFlow<UiState>, TextField-কে ড্রাইভ করবে না।
  • আপনার ViewModel-এ প্রয়োগ করা অন্য যেকোনও পারসিসটেন্স লজিক একই থাকতে পারে।