কোথায় স্টেট হোয়েস্ট করতে হবে

Compose অ্যাপ্লিকেশনে, আপনি কোথায় UI স্টেট হোস্ট করবেন তা নির্ভর করে UI লজিক বা বিজনেস লজিকের উপর। এই ডকুমেন্টে এই দুটি প্রধান পরিস্থিতি বর্ণনা করা হয়েছে।

পেশাদার পদ্ধতি

যেসব কম্পোজেবল UI স্টেট রিড ও রাইট করে, সেগুলির মধ্যে সবচেয়ে নিচের কমন অ্যানসেস্টর-এ UI স্টেট হোইস্ট করা উচিত। যেখানে স্টেট ব্যবহার করা হয়, তার সবচেয়ে কাছাকাছি স্টেট রাখতে হবে। স্টেট ওনার থেকে, গ্রাহকদের কাছে অপরিবর্তনীয় স্টেট এবং স্টেট পরিবর্তন করার জন্য ইভেন্ট প্রকাশ করুন।

সবচেয়ে নিচের কমন অ্যানসেস্টর কম্পোজিশনের বাইরেও থাকতে পারে। যেমন, ViewModel-এ স্টেট হোইস্ট করার সময় কারণ বিজনেস লজিক জড়িত।

এই পৃষ্ঠায় এই পেশাদার পদ্ধতি সম্পর্কে বিস্তারিতভাবে ব্যাখ্যা করা হয়েছে এবং একটি সতর্কতাও দেওয়া হয়েছে যা আপনাকে মনে রাখতে হবে।

UI স্টেট ও UI লজিকের ধরন

এই ডকুমেন্টে ব্যবহৃত UI স্টেট ও লজিকের ধরনগুলির সংজ্ঞা নিচে দেওয়া হল।

UI স্টেট

UI স্টেট হল এমন প্রপার্টি যা UI-এর বর্ণনা দেয়। UI স্টেট দুই ধরনের হয়:

  • স্ক্রিন UI স্টেট হল যা আপনাকে স্ক্রিনে দেখাতে হবে। যেমন, UI রেন্ডার করার জন্য প্রয়োজনীয় খবর সম্পর্কিত নিবন্ধ ও অন্যান্য তথ্য NewsUiState ক্লাসে থাকতে পারে। এই স্টেট সাধারণত হায়ারার্কির অন্যান্য লেয়ারের সাথে কানেক্ট করা থাকে কারণ এতে অ্যাপ ডেটা থাকে।
  • UI এলিমেন্টের স্ট্যাটাস বলতে UI এলিমেন্টের অন্তর্নিহিত প্রপার্টিকে বোঝায় যা সেগুলি কীভাবে রেন্ডার করা হবে তা প্রভাবিত করে। কোনও UI এলিমেন্ট দেখানো বা লুকানো হতে পারে এবং এতে নির্দিষ্ট ফন্ট, ফন্ট সাইজ বা ফন্টের রঙ থাকতে পারে। Jetpack Compose-এ, স্টেট কম্পোজেবলের বাইরে থাকে এবং আপনি এটিকে কম্পোজেবলের আশেপাশের এলাকা থেকে কলিং কম্পোজেবল ফাংশন বা স্টেট হোল্ডারে উত্তোলন করতে পারেন। এর একটি উদাহরণ হল Scaffold কম্পোজেবলের জন্য ScaffoldState।

লজিক

কোনও অ্যাপ্লিকেশনের লজিক বিজনেস লজিক বা UI লজিক হতে পারে:

  • বিজনেস লজিক হল অ্যাপ ডেটার জন্য প্রোডাক্টের প্রয়োজনীয়তা পূরণ করা। যেমন, ব্যবহারকারী বোতামে ট্যাপ করলে, কোনও নিউজ রিডার অ্যাপে কোনও নিবন্ধ বুকমার্ক করা। ফাইল বা ডেটাবেসে বুকমার্ক সেভ করার এই লজিক সাধারণত ডোমেন বা ডেটা লেয়ারে থাকে। স্টেট হোল্ডার সাধারণত এইসব লেয়ারের এক্সপোজ করা মেথড কল করার মাধ্যমে এই লজিক ডেলিগেট করে।
  • UI লজিক স্ক্রিনে UI স্টেট কীভাবে দেখানো হবে তার সাথে সম্পর্কিত। যেমন, ব্যবহারকারী কোনও বিভাগ বেছে নিলে সঠিক সার্চ বার ইঙ্গিত পাওয়া, কোনও তালিকার নির্দিষ্ট আইটেমে স্ক্রল করা অথবা ব্যবহারকারী কোনও বোতামে ক্লিক করলে নির্দিষ্ট স্ক্রিনে যাওয়ার নেভিগেশন লজিক।

UI লজিক

UI লজিক-এর যখন স্টেট রিড বা রাইট করার প্রয়োজন হয়, তখন আপনাকে স্টেটের স্কোপ UI-তে সেট করতে হবে এবং এর লাইফসাইকেল ফলো করতে হবে। এটি করতে, আপনাকে কম্পোজেবল ফাংশনে সঠিক লেভেলে স্টেট হোইস্ট করতে হবে। বিকল্প হিসেবে, আপনি প্লেন স্টেট হোল্ডার ক্লাস-এও এটি করতে পারেন, যা UI লাইফসাইকেলের স্কোপে থাকে।

নিচে দুটি সমাধানের বিবরণ এবং কখন কোনটি ব্যবহার করতে হবে তার ব্যাখ্যা দেওয়া হল।

স্টেট ওনার হিসেবে কম্পোজেবল

স্টেট ও লজিক সহজ হলে, কম্পোজেবলে UI লজিক ও UI এলিমেন্ট স্টেট থাকা ভালো। আপনি কম্পোজেবলের ইন্টার্নাল স্টেট ছেড়ে দিতে পারেন অথবা প্রয়োজন অনুযায়ী হোস্ট করতে পারেন।

স্টেট হোইস্টিংয়ের প্রয়োজন নেই

Hoisting state সবসময় প্রয়োজন হয় না। অন্য কোনও কম্পোজেবলকে এটি কন্ট্রোল করতে না হলে, কম্পোজেবলে স্টেট ইন্টার্নাল হিসেবে রাখা যেতে পারে। এই স্নিপেটে, একটি কম্পোজেবল আছে যা ট্যাপ করলে বড় ও ছোট হয়:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state

    Text(
        text = AnnotatedString(message.content),
        modifier = Modifier.clickable {
            showDetails = !showDetails // Apply UI logic
        }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

showDetails ভেরিয়েবেল হল এই UI এলিমেন্টের ইন্টার্নাল স্টেট। এটি শুধুমাত্র এই কম্পোজেবলে রিড ও পরিবর্তন করা হয় এবং এতে প্রয়োগ করা লজিক খুবই সহজ। তাই এই ক্ষেত্রে স্টেট হোইস্ট করলে খুব একটা সুবিধা হবে না, তাই আপনি এটি ইন্টার্নাল হিসেবে রেখে দিতে পারেন। এটি করলে, এই কম্পোজেবলটি মালিক এবং প্রসারিত অবস্থার একক সত্যের উৎস হয়ে ওঠে।

কম্পোজেবলের মধ্যে হোইস্টিং

আপনাকে যদি অন্য কম্পোজেবলের সাথে UI এলিমেন্টের স্ট্যাটাস শেয়ার করতে হয় এবং বিভিন্ন জায়গায় সেটির উপর UI লজিক প্রয়োগ করতে হয়, তাহলে আপনি UI হায়ারার্কিতে এটিকে আরও উপরে তুলতে পারেন। এটি আপনার কম্পোজেবলকে আরও বেশি করে ব্যবহারযোগ্য এবং পরীক্ষা করার জন্য সহজ করে তোলে।

নিম্নলিখিত উদাহরণটি হল একটি চ্যাট অ্যাপ যা দুটি কার্যকারিতা প্রয়োগ করে:

  • JumpToBottom বোতামটি মেসেজের তালিকা স্ক্রল করে একদম নিচে নিয়ে যায়। বোতামটি তালিকার স্ট্যাটাসে UI লজিক পারফর্ম করে।
  • ব্যবহারকারী নতুন মেসেজ পাঠানোর পরে MessagesList তালিকা স্ক্রল করে একদম নিচে চলে যায়। UserInput তালিকার স্ট্যাটাসে UI লজিক পারফর্ম করে।
JumpToBottom বোতাম সহ চ্যাট অ্যাপ এবং নতুন মেসেজ এলে নিচে স্ক্রল করুন
ছবি ১. JumpToBottom বোতাম সহ চ্যাট অ্যাপ এবং নতুন মেসেজ এলে স্ক্রল করে নিচে যান

কম্পোজেবল হায়ারার্কি নিচে দেওয়া হল:

চ্যাট কম্পোজেবল ট্রি
ছবি ২. চ্যাট কম্পোজেবল ট্রি

LazyColumn স্টেটটি কথোপকথন স্ক্রিনে হোস্ট করা হয় যাতে অ্যাপটি UI লজিক পারফর্ম করতে পারে এবং এটি প্রয়োজন এমন সমস্ত কম্পোজেবল থেকে স্টেটটি পড়তে পারে:

LazyColumn থেকে ConversationScreen-এ LazyColumn স্টেট হোইস্ট করা
ছবি ৩. LazyColumn থেকে ConversationScreen-এ LazyColumn স্টেট হোইস্ট করা

তাই অবশেষে কম্পোজেবলগুলি হল:

ConversationScreen-এ LazyListState সহ চ্যাট কম্পোজেবল ট্রি
ছবি ৪. LazyListState-এর সাথে চ্যাট কম্পোজেবল ট্রি ConversationScreen-এ হোস্ট করা হয়েছে

কোডটি নিচে দেওয়া হল:

@Composable
private fun ConversationScreen(/*...*/) {
    val scope = rememberCoroutineScope()

    val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen

    MessagesList(messages, lazyListState) // Reuse same state in MessageList

    UserInput(
        onMessageSent = { // Apply UI logic to lazyListState
            scope.launch {
                lazyListState.scrollToItem(0)
            }
        },
    )
}

@Composable
private fun MessagesList(
    messages: List<Message>,
    lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value
) {

    LazyColumn(
        state = lazyListState // Pass hoisted state to LazyColumn
    ) {
        items(messages, key = { message -> message.id }) { item ->
            Message(/*...*/)
        }
    }

    val scope = rememberCoroutineScope()

    JumpToBottom(onClicked = {
        scope.launch {
            lazyListState.scrollToItem(0) // UI logic being applied to lazyListState
        }
    })
}

UI লজিক প্রয়োগ করার জন্য LazyListState যতটা উপরে তোলা প্রয়োজন ততটা উপরে তোলা হয়েছে। যেহেতু এটি একটি কম্পোজেবল ফাংশনে শুরু করা হয়েছে, তাই এটি কম্পোজিশনে স্টোর করা হয়, এর লাইফসাইকেল অনুসরণ করে।

মনে রাখবেন যে lazyListState, MessagesList পদ্ধতিতে সংজ্ঞায়িত করা হয়েছে, যার ডিফল্ট ভ্যালু হল rememberLazyListState()। এটি Compose-এ একটি সাধারণ প্যাটার্ন। এটি কম্পোজেবলকে আরও বেশি পুনর্ব্যবহারযোগ্য ও নমনীয় করে তোলে। তারপর আপনি অ্যাপের বিভিন্ন অংশে কম্পোজেবল ব্যবহার করতে পারবেন, যেখানে স্টেট কন্ট্রোল করার প্রয়োজন নাও হতে পারে। সাধারণত কম্পোজেবল টেস্ট বা প্রিভিউ করার সময় এটি ঘটে। LazyColumn ঠিক এইভাবে তার অবস্থা নির্ধারণ করে।

LazyListState-এর জন্য সর্বনিম্ন সাধারণ পূর্বপুরুষ হল ConversationScreen
ছবি ৫. LazyListState-এর জন্য সর্বনিম্ন সাধারণ পূর্বপুরুষ হল ConversationScreen

স্টেটের মালিক হিসেবে প্লেন স্টেট হোল্ডার ক্লাস

যখন কোনও কম্পোজ করার উপযুক্ত আইটেমে জটিল UI লজিক থাকে যা UI এলিমেন্টের এক বা একাধিক স্টেট ফিল্ডের সাথে যুক্ত থাকে, তখন সেটির সেই দায়িত্ব স্টেট হোল্ডার-এর উপর অর্পণ করা উচিত, যেমন একটি প্লেন স্টেট হোল্ডার ক্লাস। এটি কম্পোজেবলের লজিককে আলাদাভাবে আরও বেশি পরীক্ষাযোগ্য করে তোলে এবং এর জটিলতা কমায়। এই পদ্ধতিটি সেপারেশন অফ কনসার্ন নীতি অনুসরণ করে: কম্পোজেবল UI এলিমেন্ট এমিট করার দায়িত্বে থাকে এবং স্টেট হোল্ডারে UI লজিক ও UI এলিমেন্ট স্টেট থাকে।

প্লেন স্টেট হোল্ডার ক্লাস আপনার কম্পোজেবল ফাংশনের কলারদের সুবিধাজনক ফাংশন প্রদান করে, যাতে তাদের নিজেদের এই লজিক লিখতে না হয়।

এই সাধারণ ক্লাসগুলি কম্পোজিশনে তৈরি করা হয় এবং মনে রাখা হয়। যেহেতু এগুলি কম্পোজেবলের লাইফসাইকেল অনুসরণ করে, তাই এগুলি কম্পোজ লাইব্রেরির প্রদান করা ধরন নিতে পারে, যেমন rememberNavController() বা rememberLazyListState()।

এর একটি উদাহরণ হল LazyListState প্লেন স্টেট হোল্ডার ক্লাস, যা LazyColumn বা LazyRow-এর UI জটিলতা নিয়ন্ত্রণ করতে Compose-এ প্রয়োগ করা হয়।

// LazyListState.kt

@Stable
class LazyListState constructor(
    firstVisibleItemIndex: Int = 0,
    firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
    /**
     *   The holder class for the current scroll position.
     */
    private val scrollPosition = LazyListScrollPosition(
        firstVisibleItemIndex, firstVisibleItemScrollOffset
    )

    suspend fun scrollToItem(/*...*/) { /*...*/ }

    override suspend fun scroll() { /*...*/ }

    suspend fun animateScrollToItem() { /*...*/ }
}

LazyListState, LazyColumn-এর স্টেট এনক্যাপসুলেট করে, যা এই UI এলিমেন্টের জন্য scrollPosition স্টোর করে। এছাড়াও, এটি স্ক্রল পজিশন পরিবর্তন করার পদ্ধতি দেখায়, যেমন, কোনও নির্দিষ্ট আইটেমে স্ক্রল করা।

আপনি দেখতে পাচ্ছেন, কম্পোজেবলের দায়িত্ব বাড়ালে স্টেট হোল্ডারের প্রয়োজনীয়তাও বেড়ে যায়। UI লজিক বা শুধু ট্র্যাক করার জন্য স্টেট ডেটার পরিমাণের ক্ষেত্রে এই দায়িত্বগুলি থাকতে পারে।

আরেকটি সাধারণ প্যাটার্ন হল অ্যাপে রুট কম্পোজেবল ফাংশনের জটিলতা ম্যানেজ করার জন্য একটি সাধারণ স্টেট হোল্ডার ক্লাস ব্যবহার করা। আপনি নেভিগেশন স্টেট এবং স্ক্রিনের সাইজের মতো অ্যাপ-লেভেল স্টেট এনক্যাপসুলেট করতে এই ধরনের ক্লাস ব্যবহার করতে পারেন। এই বিষয়ে সম্পূর্ণ বিবরণ UI লজিক ও তার স্টেট হোল্ডার পৃষ্ঠায় পাওয়া যাবে।

বিজনেস লজিক

কম্পোজেবল ও প্লেন স্টেট হোল্ডার ক্লাস যদি UI লজিক ও UI এলিমেন্ট স্টেটের দায়িত্বে থাকে, তাহলে স্ক্রিন লেভেল স্টেট হোল্ডার নিম্নলিখিত কাজের দায়িত্বে থাকে:

  • অ্যাপ্লিকেশনের বিজনেস লজিক-এ অ্যাক্সেস প্রদান করা যা সাধারণত হায়ারার্কির অন্যান্য লেয়ারে থাকে, যেমন বিজনেস ও ডেটা লেয়ার।
  • কোনও নির্দিষ্ট স্ক্রিনে প্রেজেন্টেশনের জন্য অ্যাপ্লিকেশন ডেটা প্রস্তুত করা, যা স্ক্রিন UI স্টেট হয়ে যায়।

স্টেটের মালিক হিসেবে ViewModels

Android ডেভেলপমেন্টে AAC ViewModels-এর সুবিধা এটিকে বিজনেস লজিকে অ্যাক্সেস প্রদান করা এবং স্ক্রিনে প্রেজেন্টেশনের জন্য অ্যাপ্লিকেশন ডেটা প্রস্তুত করার জন্য উপযুক্ত করে তোলে।

আপনি ViewModel-এ UI স্টেট হোইস্ট করলে, আপনি এটিকে কম্পোজিশনের বাইরে নিয়ে যান।

ViewModel-এ উত্তোলন করা স্টেট কম্পোজিশনের বাইরে স্টোর করা হয়।
ছবি ৬. ViewModel-এ স্টেট হোইস্ট করা হলে তা কম্পোজিশনের বাইরে স্টোর করা হয়।

ViewModels কম্পোজিশনের অংশ হিসেবে স্টোর করা হয় না। এগুলি ফ্রেমওয়ার্কের মাধ্যমে প্রদান করা হয় এবং এগুলি ViewModelStoreOwner-এর স্কোপে থাকে যা অ্যাক্টিভিটি, ফ্র্যাগমেন্ট, নেভিগেশন গ্রাফ বা নেভিগেশন গ্রাফের ডেস্টিনেশন হতে পারে। ViewModel স্কোপ সম্পর্কে আরও তথ্যের জন্য আপনি ডকুমেন্টেশন পর্যালোচনা করতে পারেন।

তারপর, ViewModel হল UI স্টেট-এর জন্য সত্যের উৎস এবং সর্বনিম্ন সাধারণ পূর্বপুরুষ।

স্ক্রিন UI স্টেট

উপরের সংজ্ঞা অনুযায়ী, বিজনেস রুল প্রয়োগ করে স্ক্রিন UI স্টেট তৈরি করা হয়। স্ক্রিন লেভেল স্টেট হোল্ডার যেহেতু এর জন্য দায়ী, তাই এর অর্থ হল স্ক্রিন UI স্টেট সাধারণত স্ক্রিন লেভেল স্টেট হোল্ডারে হোস্ট করা হয়, এই ক্ষেত্রে ViewModel।

চ্যাট অ্যাপের ConversationViewModel এবং এটি কীভাবে স্ক্রিন UI স্টেট ও ইভেন্টকে মডিফাই করার জন্য এক্সপোজ করে তা বিবেচনা করুন:

class ConversationViewModel(
    channelId: String,
    messagesRepository: MessagesRepository
) : ViewModel() {

    val messages = messagesRepository
        .getLatestMessages(channelId)
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )

    // Business logic
    fun sendMessage(message: Message) { /* ... */ }
}

ViewModel-এ স্ক্রিন UI স্টেট হোস্ট করা কম্পোজেবল ব্যবহার করে। বিজনেস লজিকে অ্যাক্সেস দিতে, আপনার স্ক্রিন-লেভেল কম্পোজেবলে ViewModel ইনস্ট্যান্স ইনজেক্ট করতে হবে।

স্ক্রিন-লেভেল কম্পোজেবলে ব্যবহার করা ViewModel-এর উদাহরণ নিচে দেওয়া হল। এখানে, কম্পোজেবল ConversationScreen(), ViewModel-এ স্ক্রিন UI স্টেট উত্তোলন ব্যবহার করে:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

প্রপার্টি ড্রিলিং

"প্রপার্টি ড্রিলিং" বলতে বোঝায়, একাধিক নেস্টেড চাইল্ড কম্পোনেন্টের মাধ্যমে ডেটা পাস করে সেই লোকেশনে নিয়ে যাওয়া যেখানে সেগুলি পড়া হয়।

Compose-এ প্রপার্টি ড্রিলিংয়ের একটি সাধারণ উদাহরণ হল, আপনি যখন টপ লেভেলে স্ক্রিন লেভেল স্টেট হোল্ডার ইনজেক্ট করেন এবং চাইল্ড কম্পোজেবলে স্টেট ও ইভেন্ট পাস করেন। এটি কম্পোজেবল ফাংশন সিগনেচারের ওভারলোড অতিরিক্তভাবে তৈরি করতে পারে।

যদিও ইভেন্টগুলিকে স্বতন্ত্র ল্যাম্বডা প্যারামিটার হিসাবে প্রকাশ করলে ফাংশন সিগনেচার ওভারলোড হতে পারে, এটি কম্পোজেবল ফাংশনের দায়িত্বগুলি সর্বাধিক দৃশ্যমান করে। এটি কী করে তা আপনি এক নজরে দেখতে পারবেন।

স্টেট ও ইভেন্ট এক জায়গায় এনক্যাপসুলেট করার জন্য র‍্যাপার ক্লাস তৈরি করার চেয়ে প্রপার্টি ড্রিলিং করা বেশি ভালো, কারণ এটি কম্পোজেবল রেসপন্সিবিলিটির দৃশ্যমানতা কমায়। র‍্যাপার ক্লাস না থাকার কারণে, আপনি কম্পোজেবলকে শুধুমাত্র প্রয়োজনীয় প্যারামিটার পাস করার সম্ভাবনা বেশি, যা একটি পেশাদার পদ্ধতি।

এই ইভেন্টগুলি নেভিগেশন ইভেন্ট হলে একই পেশাদার পদ্ধতি প্রযোজ্য হয়, আপনি নেভিগেশন ডকুমেন্ট থেকে এই বিষয়ে আরও জানতে পারবেন।

আপনি পারফর্ম্যান্স সংক্রান্ত সমস্যা শনাক্ত করলে, স্টেট রিডিং স্থগিত করার বিকল্পও বেছে নিতে পারেন। আরও জানতে আপনি পারফর্ম্যান্স সংক্রান্ত ডকুমেন্ট চেক করতে পারেন।

UI এলিমেন্টের স্টেট

আপনি UI এলিমেন্ট স্টেটকে স্ক্রিন লেভেল স্টেট হোল্ডারে হোস্ট করতে পারেন যদি সেখানে বিজনেস লজিক থাকে যা এটি পড়তে বা লিখতে পারে।

চ্যাট অ্যাপের উদাহরণটি চালিয়ে গিয়ে, ব্যবহারকারী @ এবং একটি ইঙ্গিত টাইপ করলে, অ্যাপটি গ্রুপ চ্যাটে ব্যবহারকারীর সাজেশন দেখায়। এইসব সাজেশন ডেটা লেয়ার থেকে আসে এবং ব্যবহারকারীর সাজেশনের তালিকা গণনা করার লজিককে বিজনেস লজিক হিসেবে বিবেচনা করা হয়। ফিচারটি দেখতে এইরকম:

ব্যবহারকারী `@` এবং কোনও ইঙ্গিত টাইপ করলে গ্রুপ চ্যাটে ব্যবহারকারীর সাজেশন দেখানোর ফিচার
ছবি ৭. ব্যবহারকারী @ এবং একটি ইঙ্গিত টাইপ করলে গ্রুপ চ্যাটে ব্যবহারকারীর সাজেশন দেখানোর ফিচার

এই ফিচার প্রয়োগকারী ViewModel-কে নিম্নলিখিতভাবে দেখা যাবে:

class ConversationViewModel(/*...*/) : ViewModel() {

    // Hoisted state
    var inputMessage by mutableStateOf("")
        private set

    val suggestions: StateFlow<List<Suggestion>> =
        snapshotFlow { inputMessage }
            .filter { hasSocialHandleHint(it) }
            .mapLatest { getHandle(it) }
            .mapLatest { repository.getSuggestions(it) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = emptyList()
            )

    fun updateInput(newInput: String) {
        inputMessage = newInput
    }
}

inputMessage হল একটি ভেরিয়েবেল যা TextField স্ট্যাটাস সেভ করে। ব্যবহারকারী যতবার নতুন ইনপুট টাইপ করেন, ততবার অ্যাপটি suggestions তৈরি করার জন্য বিজনেস লজিক কল করে।

suggestions হল স্ক্রিন UI স্টেট এবং StateFlow থেকে সংগ্রহ করে Compose UI থেকে ব্যবহার করা হয়।

Caveat

কিছু Compose UI এলিমেন্ট স্টেটের জন্য, ViewModel-এ উত্তোলন করার জন্য বিশেষ বিবেচনার প্রয়োজন হতে পারে। যেমন, Compose UI এলিমেন্টের কিছু স্টেট হোল্ডার স্টেট পরিবর্তন করার পদ্ধতি দেখায়। এর মধ্যে কিছু সাসপেন্ড ফাংশন হতে পারে যা অ্যানিমেশন ট্রিগার করে। আপনি যদি কম্পোজিশনের স্কোপে নেই এমন CoroutineScope থেকে এইসব সাসপেন্ড ফাংশন কল করেন, তাহলে ব্যতিক্রম হতে পারে।

ধরা যাক, অ্যাপ ড্রয়ারের কন্টেন্ট ডায়নামিক এবং এটি বন্ধ হওয়ার পরে আপনাকে ডেটা লেয়ার থেকে এটি ফেচ ও রিফ্রেশ করতে হবে। আপনাকে ড্রয়ার স্টেটকে ViewModel-এ হোইস্ট করতে হবে যাতে আপনি স্টেট ওনার থেকে এই এলিমেন্টে UI ও বিজনেস লজিক উভয়কেই কল করতে পারেন।

তবে, Compose UI থেকে viewModelScope ব্যবহার করে DrawerState-এর close() মেথড কল করলে, রানটাইম এক্সেপশন হয়, যার ধরন হল IllegalStateException এবং মেসেজে লেখা থাকে, “এই CoroutineContext”-এ MonotonicFrameClock উপলভ্য নেই।

এই সমস্যার সমাধান করতে, কম্পোজিশনের জন্য স্কোপ করা CoroutineScope ব্যবহার করুন। এটি CoroutineContext-এ একটি MonotonicFrameClock প্রদান করে যা সাসপেন্ড ফাংশন কাজ করার জন্য প্রয়োজন।

এই ক্র্যাশ ঠিক করতে, ViewModel-এ কো-রুটিনের CoroutineContext-কে এমন একটিতে পরিবর্তন করুন যা কম্পোজিশনের স্কোপে রয়েছে। এটি দেখতে এইরকম হতে পারে:

class ConversationViewModel(/*...*/) : ViewModel() {

    val drawerState = DrawerState(initialValue = DrawerValue.Closed)

    private val _drawerContent = MutableStateFlow(DrawerContent.Empty)
    val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow()

    fun closeDrawer(uiScope: CoroutineScope) {
        viewModelScope.launch {
            withContext(uiScope.coroutineContext) { // Use instead of the default context
                drawerState.close()
            }
            // Fetch drawer content and update state
            _drawerContent.update { content }
        }
    }
}

// in Compose
@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {
    val scope = rememberCoroutineScope()

    ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) })
}

আরও জানুন

স্টেট ও Jetpack Compose সম্পর্কে আরও জানতে, নিম্নলিখিত অতিরিক্ত রিসোর্স দেখুন।

নমুনা

কোডল্যাবস

ভিডিও