যেখানে রাজ্য উত্তোলন করতে হবে

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

সর্বোত্তম অনুশীলন

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

সর্বনিম্ন সাধারণ পূর্বপুরুষ কম্পোজিশনের বাইরেও থাকতে পারে। উদাহরণস্বরূপ, যখন ব্যবসায়িক যুক্তি জড়িত থাকার কারণে একটি ViewModel এ স্টেট হোয়িস্ট করা হয়।

এই পৃষ্ঠায় এই সর্বোত্তম অনুশীলনটি বিস্তারিতভাবে ব্যাখ্যা করা হয়েছে এবং মনে রাখার মতো একটি সতর্কবাণীও উল্লেখ করা হয়েছে।

UI স্টেট এবং UI লজিকের প্রকারভেদ

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

UI অবস্থা

UI স্টেট হলো সেই বৈশিষ্ট্য যা UI-কে বর্ণনা করে। UI স্টেট দুই প্রকারের হয়:

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

যুক্তি

একটি অ্যাপ্লিকেশনের লজিক বিজনেস লজিক অথবা UI লজিক হতে পারে:

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

UI লজিক

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

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

রাষ্ট্রীয় মালিক হিসাবে গঠনযোগ্য

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

রাষ্ট্রীয় উত্তোলনের প্রয়োজন নেই

স্টেট হোইস্ট করা সবসময় প্রয়োজন হয় না। যখন অন্য কোনো কম্পোজেবলের এটিকে নিয়ন্ত্রণ করার প্রয়োজন হয় না, তখন স্টেটকে একটি কম্পোজেবলের ভেতরে অভ্যন্তরীণভাবে রাখা যেতে পারে। এই কোড স্নিপেটটিতে, এমন একটি কম্পোজেবল রয়েছে যা ট্যাপ করলে প্রসারিত এবং সংকুচিত হয়:

@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 বাটন এবং স্ক্রল করে নিচে যাওয়ার সুবিধা রয়েছে।

কম্পোজেবল হায়ারার্কিটি নিম্নরূপ:

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

LazyColumn স্টেটটি কনভারসেশন স্ক্রিনে হোইস্ট করা হয়, যাতে অ্যাপটি UI লজিক সম্পাদন করতে পারে এবং প্রয়োজনীয় সমস্ত কম্পোজেবল থেকে স্টেটটি পড়তে পারে:

LazyColumn থেকে ConversationScreen-এ LazyColumn-এর স্টেট উত্তোলন করা
চিত্র ৩. LazyColumn থেকে ConversationScreenLazyColumn স্টেট স্থানান্তর করা

সুতরাং পরিশেষে সংযোজনযোগ্য বিষয়গুলো হলো:

LazyListState ব্যবহার করে ConversationScreen-এ হোস্ট করা চ্যাট কম্পোজেবল ট্রি
চিত্র ৪. 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 অবস্থা হয়ে ওঠে।

স্টেট মালিক হিসাবে ভিউমডেল

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

যখন আপনি ViewModel এ UI স্টেট হোইস্ট করেন, তখন সেটিকে Composition-এর বাইরে নিয়ে যাওয়া হয়।

ViewModel-এ হোইস্ট করা স্টেট Composition-এর বাইরে সংরক্ষিত থাকে।
চিত্র ৬। ViewModel এ হোইস্ট করা স্টেট Composition-এর বাইরে সংরক্ষিত থাকে।

ViewModel-গুলো Composition-এর অংশ হিসেবে সংরক্ষিত থাকে না। এগুলো ফ্রেমওয়ার্ক দ্বারা সরবরাহ করা হয় এবং একটি ViewModelStoreOwner স্কোপের মধ্যে থাকে, যা একটি Activity, Fragment, নেভিগেশন গ্রাফ বা কোনো নেভিগেশন গ্রাফের গন্তব্য হতে পারে। 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)
    /* ... */
}

সম্পত্তি খনন

“প্রপার্টি ড্রিলিং” বলতে একাধিক নেস্টেড চাইল্ড কম্পোনেন্টের মধ্য দিয়ে ডেটাকে সেই স্থানে প্রেরণ করাকে বোঝায়, যেখান থেকে তা পড়া হয়।

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

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

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

এই ইভেন্টগুলো নেভিগেশন ইভেন্ট হলেও একই সেরা অনুশীলন প্রযোজ্য, আপনি নেভিগেশন ডক্স- এ এ সম্পর্কে আরও জানতে পারবেন।

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

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 থেকে সংগ্রহ করে কম্পোজ UI দ্বারা ব্যবহৃত হয়।

সতর্কতা

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

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

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

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

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

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

আরও জানুন

স্টেট এবং জেটপ্যাক কম্পোজ সম্পর্কে আরও জানতে, নিম্নলিখিত অতিরিক্ত রিসোর্সগুলো দেখুন।

নমুনা

কোডল্যাবস

ভিডিও

{% হুবহু %} {% endverbatim %} {% হুবহু %} {% endverbatim %}