রাজ্য-ভিত্তিক পাঠ্য ক্ষেত্রে স্থানান্তর করুন

এই পৃষ্ঠায় ভ্যালু-ভিত্তিক 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() দিয়ে প্রতিস্থাপন করুন।
  • onValueChangeInputTransformation ব্যবহার করে ফিল্টারিং লজিকটি পুনরায় প্রয়োগ করুন।
  • state আপডেট করার জন্য InputTransformation এর রিসিভার স্কোপ থেকে TextFieldBuffer ব্যবহার করুন।
    • ব্যবহারকারীর ইনপুট শনাক্ত হওয়ার ঠিক পরেই 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 স্বয়ংক্রিয়ভাবে অফসেট ম্যাপিংয়ের কাজটি করে দেয়। আপনাকে শুধু 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 গণনা করার লজিক পরিবর্তন করুন।
    • বর্তমান নির্বাচনের উপর ভিত্তি করে বিদ্যমান টেক্সটকে বিভক্ত করে এবং এর মাঝে মার্কডাউন অলঙ্করণগুলো সন্নিবেশ করে একটি নতুন TextFieldValue তৈরি করা হয়।
    • এছাড়াও, পাঠ্যের নতুন সূচক অনুসারে নির্বাচনটি সামঞ্জস্য করা হয়।
    • TextFieldState.edit মাধ্যমে TextFieldBuffer ব্যবহার করে বর্তমান অবস্থা সম্পাদনা করার একটি আরও স্বাভাবিক উপায় রয়েছে।
    • এই নির্বাচনের মাধ্যমে স্পষ্টভাবে নির্ধারণ করা হয় কোথায় অলঙ্করণগুলো যুক্ত করতে হবে।
    • তারপর, onValueChange পদ্ধতির অনুরূপভাবে নির্বাচনটি সমন্বয় করুন।

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

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

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

MutableState বা TextFieldState মতো ক্লাসগুলো হলো সাধারণ স্টেট হোল্ডার, যা Compose-এর Snapshot স্টেট সিস্টেম দ্বারা সমর্থিত। এগুলো 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(usernameState) {
            snapshotFlow { usernameState.text.toString() }.collectLatest {
                loginViewModel.updatePassword(it)
            }
        }
        SecureTextField(passwordState)
    }
}

  • আপনার ViewModel এবং UiState ক্লাসগুলো একই রাখুন।
  • স্টেটকে সরাসরি ViewModel এ তুলে নিয়ে TextFields জন্য সেটিকে তথ্যের উৎস বানানোর পরিবর্তে, ViewModel একটি সাধারণ ডেটা হোল্ডারে পরিণত করুন।
    • এটি করার জন্য, একটি LaunchedEffectsnapshotFlow সংগ্রহ করে প্রতিটি TextFieldState.text এর পরিবর্তনগুলো পর্যবেক্ষণ করুন।
  • আপনার ViewModel UI থেকে সর্বশেষ মানগুলো ঠিকই থাকবে, কিন্তু এর uiState: StateFlow<UiState> TextField চালনা করবে না।
  • আপনার ViewModel এ প্রয়োগ করা অন্য যেকোনো ডেটা সংরক্ষণের যুক্তি অপরিবর্তিত থাকতে পারে।