Compose-এ UI স্টেট সেভ করা

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

অ্যাক্টিভিটি বা প্রসেস রিক্রিয়েশনের কারণে যেকোনও Android অ্যাপ তার UI স্টেট হারাতে পারে। নিম্নলিখিত ইভেন্টের কারণে এই স্টেট হারিয়ে যেতে পারে:

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

এই পৃষ্ঠায়, UI স্টেট কোথায় হোস্ট করা হয়েছে এবং কোন লজিকের জন্য এটি প্রয়োজন তার উপর নির্ভর করে UI স্টেট সেভ করার জন্য উপলভ্য API-এর সারসংক্ষেপ দেওয়া হয়েছে।

UI লজিক

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

নিচে দেওয়া স্নিপেটে, একটি বুলিয়ান UI এলিমেন্টের স্টেট স্টোর করতে rememberSaveable ব্যবহার করা হয়েছে:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

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

ছবি ১. ট্যাপ করলে চ্যাট মেসেজ বাবল বড় হয় ও ছোট হয়ে যায়।

showDetails হল একটি বুলিয়ান ভেরিয়েবল যা চ্যাট বাবলটি আড়াল করা আছে নাকি বড় করা আছে তা স্টোর করে।

সেভ করা ইনস্ট্যান্স স্টেট মেকানিজমের মাধ্যমে Bundle-এ rememberSaveable স্টোর UI এলিমেন্ট স্টেট সেভ করে।

এটি অটোমেটিক বান্ডেলে আদিম টাইপ স্টোর করতে পারে। আপনার স্টেট যদি এমন কোনও টাইপে হোল্ড করা থাকে যা আদিম নয়, যেমন ডেটা ক্লাস, তাহলে আপনি বিভিন্ন স্টোরিং মেকানিজম ব্যবহার করতে পারেন, যেমন Parcelize অ্যানোটেশন ব্যবহার করা, listSaver ও mapSaver-এর মতো Compose API ব্যবহার করা অথবা Compose রানটাইম Saver ক্লাস এক্সটেন্ড করে কাস্টম সেভার ক্লাস প্রয়োগ করা। এইসব পদ্ধতি সম্পর্কে আরও জানতে স্টেট সেভ করার উপায় ডকুমেন্টেশন দেখুন।

নিচের স্নিপেটে, rememberLazyListState Compose API LazyListState স্টোর করে, যার মধ্যে LazyColumn বা LazyRow-এর স্ক্রল স্টেট থাকে, যা rememberSaveable ব্যবহার করে। এটি একটি LazyListState.Saver ব্যবহার করে, যা একটি কাস্টম সেভার যা স্ক্রল স্টেট সেভ ও রিস্টোর করতে পারে। অ্যাক্টিভিটি বা প্রসেস রিক্রিয়েশনের পরে (যেমন, ডিভাইসের ওরিয়েন্টেশন পরিবর্তন করার মতো কনফিগারেশন পরিবর্তনের পরে), স্ক্রল স্টেট সংরক্ষিত থাকে।

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

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

rememberSaveable UI স্টেট সেভ করার জন্য Bundle ব্যবহার করে, যা অন্যান্য API-এর সাথে শেয়ার করা হয়। এগুলিও এতে লেখে, যেমন আপনার অ্যাক্টিভিটিতে onSaveInstanceState() কল। তবে, এই Bundle-এর সাইজ সীমিত এবং বড় অবজেক্ট স্টোর করলে রানটাইমে TransactionTooLarge ব্যতিক্রম হতে পারে। এটি বিশেষ করে সিঙ্গেল Activity অ্যাপের ক্ষেত্রে সমস্যা তৈরি করতে পারে, যেখানে একই Bundle অ্যাপ জুড়ে ব্যবহার করা হচ্ছে।

এই ধরনের ক্র্যাশ এড়াতে, বান্ডেলে বড় ও জটিল অবজেক্ট বা অবজেক্টের তালিকা স্টোর করা উচিত নয়।

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

এইসব ডিজাইন সংক্রান্ত পছন্দ আপনার অ্যাপের নির্দিষ্ট ব্যবহারের ক্ষেত্রে এবং আপনার ব্যবহারকারীরা এটি কীভাবে ব্যবহার করতে চান তার উপর নির্ভর করে।

স্টেট ফিরিয়ে আনা যাচাই করা

অ্যাক্টিভিটি বা প্রসেস আবার তৈরি করা হলে, আপনার কম্পোজ এলিমেন্টে rememberSaveable-এর সাথে সেভ করা স্টেট সঠিকভাবে রিস্টোর করা হয়েছে কিনা তা আপনি যাচাই করতে পারবেন। এটি করার জন্য নির্দিষ্ট API আছে, যেমন StateRestorationTester। আরও জানতে পরীক্ষা সংক্রান্ত ডকুমেন্টেশন দেখুন।

বিজনেস লজিক

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

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

তবে, সিস্টেম-ইনিশিয়েটেড প্রসেস ডেথ হলে ViewModel ইনস্ট্যান্স আর থাকে না। এটি যাতে UI স্টেটকে প্রভাবিত করতে না পারে, তার জন্য ViewModel-এর জন্য সেভ করা স্টেট মডিউল ব্যবহার করুন, যার মধ্যে SavedStateHandle API থাকে।

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

SavedStateHandle UI স্টেট সেভ করার জন্য Bundle মেকানিজমও ব্যবহার করে, তাই আপনাকে শুধুমাত্র সাধারণ UI এলিমেন্ট স্টেট সেভ করার জন্য এটি ব্যবহার করা উচিত।

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

SavedStateHandle API

SavedStateHandle-এর UI এলিমেন্ট স্টেট সেভ করার জন্য আলাদা আলাদা API আছে, এর মধ্যে সবচেয়ে উল্লেখযোগ্য হল:

কম্পোজ করা State saveable()
StateFlow getStateFlow()

কম্পোজ করা State

UI এলিমেন্টের স্টেট MutableState হিসেবে রিড ও রাইট করতে saveable SavedStateHandle-এর API ব্যবহার করুন, যাতে এটি ন্যূনতম কোড সেট-আপের মাধ্যমে অ্যাক্টিভিটি ও প্রসেস রিক্রিয়েশন সারভাইভ করতে পারে।

saveable API, বক্সের বাইরেই আদিম ধরনের সাথে কাজ করে এবং কাস্টম সেভার ব্যবহার করার জন্য একটি stateSaver প্যারামিটার পায়, ঠিক rememberSaveable()-এর মতো।

নিচে দেওয়া স্নিপেটে, message ব্যবহারকারীর ইনপুট স্টোর করে যা TextField-এ টাইপ করা হয়:

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

saveable API ব্যবহার করা সম্পর্কে আরও তথ্য পেতে SavedStateHandle ডকুমেন্টেশন দেখুন।

StateFlow

UI এলিমেন্ট স্টেট স্টোর করতে getStateFlow() ব্যবহার করুন এবং SavedStateHandle থেকে ফ্লো হিসেবে এটি ব্যবহার করুন। StateFlow হল শুধু-পঠনযোগ্য, এবং API-এর জন্য আপনাকে একটি কী উল্লেখ করতে হবে যাতে আপনি ফ্লো পরিবর্তন করে নতুন ভ্যালু এমিট করতে পারেন। আপনার কনফিগার করা কী ব্যবহার করে, আপনি StateFlow রিট্রিভ করতে এবং লেটেস্ট ভ্যালু সংগ্রহ করতে পারবেন।

নিচের স্নিপেটে, savedFilterType হল একটি StateFlow ভেরিয়েবল যা চ্যাট অ্যাপে চ্যাট চ্যানেলের তালিকায় প্রয়োগ করা ফিল্টারের ধরন স্টোর করে:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

ব্যবহারকারী প্রতিবার নতুন ফিল্টার ধরন বেছে নিলে, setFiltering কল করা হয়। এটি _CHANNEL_FILTER_SAVED_STATE_KEY_ কী-এর সাথে সেভ করা SavedStateHandle-এ নতুন ভ্যালু সেভ করে। savedFilterType হল এমন একটি ফ্লো যা কী-তে সেভ করা লেটেস্ট ভ্যালু এমিট করে। filteredChannels চ্যানেল ফিল্টার করার জন্য ফ্লোতে সাবস্ক্রাইব করেছে।

getStateFlow() API সম্পর্কে আরও জানতে SavedStateHandle ডকুমেন্টেশন দেখুন।

সারাংশ

নিম্নলিখিত সারণীতে এই বিভাগে অন্তর্ভুক্ত API-এর সংক্ষিপ্তসার এবং UI স্টেট সেভ করার জন্য কোন API কখন ব্যবহার করতে হবে তা দেখানো হয়েছে:

ইভেন্ট UI লজিক ViewModel-এ বিজনেস লজিক
কনফিগারেশনে পরিবর্তন rememberSaveable অটোমেটিক
সিস্টেম-ইনিশিয়েটেড প্রসেস ডেথ rememberSaveable SavedStateHandle

কোথায় স্টেট অনুষ্ঠিত হচ্ছে এবং এর জন্য কী লজিক প্রয়োজন, তার উপর নির্ভর করে কোন API ব্যবহার করতে হবে। UI লজিকে ব্যবহৃত স্টেটের জন্য, rememberSaveable ব্যবহার করুন। বিজনেস লজিকে ব্যবহৃত স্টেটের জন্য, আপনি যদি এটি ViewModel-এ হোল্ড করেন, SavedStateHandle ব্যবহার করে এটি সেভ করুন।

আপনাকে UI স্টেটের অল্প পরিমাণ ডেটা স্টোর করতে বান্ডেল API (rememberSaveable ও SavedStateHandle) ব্যবহার করতে হবে। এই ডেটা হল UI-কে আগের অবস্থায় ফিরিয়ে আনার জন্য ন্যূনতম প্রয়োজনীয় ডেটা, এর সাথে অন্যান্য স্টোরিং মেকানিজমও থাকে। যেমন, আপনি যদি বান্ডেলে ব্যবহারকারী যে প্রোফাইল দেখছেন তার আইডি স্টোর করেন, তাহলে ডেটা লেয়ার থেকে প্রোফাইলের বিবরণের মতো ভারী ডেটা ফেচ করতে পারবেন।

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