অ্যান্ড্রয়েড জেটপ্যাকের অংশ, ViewModel-এর জন্য সংরক্ষিত অবস্থা মডিউল।

UI স্টেট সংরক্ষণ (Saving UI States) অংশে যেমন উল্লেখ করা হয়েছে, ViewModel অবজেক্টগুলো কনফিগারেশন পরিবর্তন সামলাতে পারে, তাই রোটেশন বা অন্যান্য ক্ষেত্রে স্টেট নিয়ে আপনাকে চিন্তা করতে হবে না। তবে, যদি সিস্টেম-জনিত প্রসেস বন্ধ হয়ে যাওয়া সামলানোর প্রয়োজন হয়, তাহলে ব্যাকআপ হিসেবে আপনি SavedStateHandle API ব্যবহার করতে পারেন।

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

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

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

সেটআপ

SavedStateHandle ব্যবহার করতে হলে, এটিকে আপনার ViewModel এর কনস্ট্রাক্টর আর্গুমেন্ট হিসেবে গ্রহণ করুন।

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

এরপর আপনি কোনো অতিরিক্ত কনফিগারেশন ছাড়াই আপনার কম্পোজেবলগুলোর মধ্যে আপনার ViewModel এর একটি ইনস্ট্যান্স পেতে পারেন। ডিফল্ট ViewModel ফ্যাক্টরি আপনার ViewModel জন্য উপযুক্ত SavedStateHandle প্রদান করে।

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

একটি কাস্টম ViewModelProvider.Factory ইনস্ট্যান্স প্রদান করার সময়, আপনি CreationExtras এবং viewModelFactory DSL ব্যবহার করে SavedStateHandle এর ব্যবহার সক্ষম করতে পারেন।

SavedStateHandle নিয়ে কাজ করা

SavedStateHandle ক্লাসটি একটি কী-ভ্যালু ম্যাপ, যা আপনাকে set() এবং get() মেথডের মাধ্যমে সংরক্ষিত স্টেটে ডেটা লিখতে এবং সেখান থেকে ডেটা পুনরুদ্ধার করতে দেয়।

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

একটি কী-ভ্যালু ম্যাপের সাথে কাজ করার সময় আপনি SavedStateHandle আরও কিছু মেথড আশা করতে পারেন:

  • contains(String key) - প্রদত্ত কী-টির জন্য কোনো ভ্যালু আছে কিনা তা যাচাই করে।
  • remove(String key) - প্রদত্ত কী-এর মান মুছে দেয়।
  • keys() - SavedStateHandle এর অন্তর্ভুক্ত সমস্ত কী ফেরত দেয়।

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

স্টেটফ্লো

আপনি একটি StateFlow অবজার্ভেবলের মধ্যে থাকা SavedStateHandle থেকে মান পুনরুদ্ধার করতে পারেন। আপনার সরাসরি মান পরিবর্তন করার প্রয়োজন আছে কিনা তার উপর নির্ভর করে, আপনি একটি রিড-অনলি বা মিউটেবল স্ট্রিমের মধ্যে থেকে বেছে নিতে পারেন:

  • getStateFlow() : শুধুমাত্র স্টেট পড়ার প্রয়োজন হলে এটি ব্যবহার করুন। যখন আপনি SavedStateHandle এর অন্য কোথাও কী-এর ভ্যালু আপডেট করেন, তখন StateFlow নতুন ভ্যালুটি গ্রহণ করে। যখন আপনি একটি রিড-অনলি স্ট্রিম প্রকাশ করতে এবং ফ্লো অপারেটর ব্যবহার করে এটিকে রূপান্তর করতে চান, তখন এটি আদর্শ।
  • getMutableStateFlow() : আপনার যদি রিড এবং রাইট উভয় অ্যাক্সেসের প্রয়োজন হয় তবে এটি ব্যবহার করুন। রিটার্ন করা MutableStateFlow এর .value আপডেট করলে অন্তর্নিহিত SavedStateHandle স্বয়ংক্রিয়ভাবে আপডেট হয়ে যায়, ফলে আপনাকে ম্যানুয়ালি কী (key) সেট করার প্রয়োজন হয় না।

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

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    // Use getMutableStateFlow to read and write the query directly
    private val _query = savedStateHandle.getMutableStateFlow("query", "")
    val query: StateFlow = _query.asStateFlow()

    // Use getStateFlow if you only need a read-only stream to react to changes
    val filteredData: StateFlow<List> =
        query.flatMapLatest {
            repository.getFilteredData(it)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun setQuery(newQuery: String) {
        // Updating the MutableStateFlow automatically updates the SavedStateHandle
        _query.value = newQuery
    }
}

KotlinX সিরিয়ালাইজেশন সমর্থন

জটিল UI স্টেটের জন্য, আপনি KotlinX Serialization-এর পাশাপাশি saved প্রপার্টি ডেলিগেট ব্যবহার করতে পারেন। এই ডেলিগেটটি আপনাকে কাস্টম @Serializable ডেটা ক্লাসগুলোকে সরাসরি SavedStateHandle এ পারসিস্ট করতে দেয়। এটি প্রসেস বন্ধ হয়ে যাওয়ার পরেও আপনার ViewModel-এর স্টেট সংরক্ষণ করে, ফলে পুনরায় চালু হওয়ার পর আপনার Compose UI নির্বিঘ্নে তার স্টেট পুনরুদ্ধার করতে পারে।

এটি ব্যবহার করতে, আপনার ডেটা ক্লাসকে @Serializable দিয়ে অ্যানোটেট করুন এবং আপনার ViewModel-এ saved ডেলিগেটটি ব্যবহার করুন:

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
// Ensure you have the savedstate-ktx dependency
import androidx.savedstate.serialization.saved
import kotlinx.serialization.Serializable

@Serializable
data class UserFilterState(
    val searchQuery: String,
    val minAge: Int,
    val includeInactive: Boolean
)

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

    // The state is automatically serialized to a Bundle on process death,
    // and deserialized upon recreation.
    var filterState by savedStateHandle.saved {
        UserFilterState(searchQuery = "", minAge = 18, includeInactive = false)
    }

    fun updateQuery(newQuery: String) {
        // Mutating the property automatically updates the underlying SavedStateHandle
        filterState = filterState.copy(searchQuery = newQuery)
    }
}

রাষ্ট্রীয় সমর্থন রচনা করুন

যদি আপনার স্টেট KotlinX Serialization-এর পরিবর্তে Compose-এর Saver API-এর উপর নির্ভর করে, তাহলে lifecycle-viewmodel-compose আর্টিফ্যাক্টটি saveable ডেলিগেট প্রদান করে। এটি SavedStateHandle এবং Compose-এর Saver মধ্যে আন্তঃকার্যক্ষমতা নিশ্চিত করে, যার ফলে rememberSaveable ব্যবহার করে একটি কাস্টম Saver মাধ্যমে যে কোনো State সেভ করা গেলে, তা SavedStateHandle ব্যবহার করেও সেভ করা যায়।

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    var filteredData: List<String> by savedStateHandle.saveable {
        mutableStateOf(emptyList())
    }

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData += query
        }
    }
}

সমর্থিত প্রকার

একটি SavedStateHandle মধ্যে থাকা ডেটা আপনার অ্যাপের বাকি savedInstanceState এর সাথে একটি Bundle হিসেবে সংরক্ষিত ও পুনরুদ্ধার করা হয়।

সরাসরি সমর্থিত প্রকারগুলি

ডিফল্টরূপে, আপনি একটি Bundle মতো একই ডেটা টাইপের জন্য একটি SavedStateHandle উপর set() এবং get() কল করতে পারেন, যেমনটি নিচে দেখানো হয়েছে:

প্রকার/শ্রেণী সমর্থন অ্যারে সমর্থন
double double[]
int int[]
long long[]
String String[]
byte byte[]
char char[]
CharSequence CharSequence[]
float float[]
Parcelable Parcelable[]
Serializable Serializable[]
short short[]
SparseArray
Binder
Bundle
ArrayList
Size (only in API 21+)
SizeF (only in API 21+)

যদি ক্লাসটি উপরের তালিকার কোনোটি এক্সটেন্ড না করে, তাহলে @Parcelize কোটলিন অ্যানোটেশন যোগ করে অথবা সরাসরি Parcelable ইমপ্লিমেন্ট করে ক্লাসটিকে পার্সেলযোগ্য করার কথা বিবেচনা করতে পারেন।

অ-পার্সেলযোগ্য ক্লাস সংরক্ষণ করা

যদি কোনো ক্লাস Parcelable বা Serializable ইন্টারফেস ইমপ্লিমেন্ট না করে এবং সেটিকে পরিবর্তন করে এই ইন্টারফেসগুলোর কোনো একটি ইমপ্লিমেন্ট করানো না যায়, তাহলে সেই ক্লাসের কোনো ইনস্ট্যান্সকে সরাসরি SavedStateHandle এ সেভ করা সম্ভব নয়।

লাইফসাইকেল 2.3.0-alpha03 থেকে শুরু করে, SavedStateHandle আপনাকে setSavedStateProvider() মেথড ব্যবহার করে আপনার অবজেক্টকে একটি Bundle (Bundle) হিসেবে সংরক্ষণ ও পুনরুদ্ধার করার জন্য নিজস্ব লজিক যোগ করে যেকোনো অবজেক্ট সংরক্ষণ করার সুযোগ দেয়। SavedStateRegistry.SavedStateProvider হলো একটি ইন্টারফেস যা একটিমাত্র saveState() মেথড সংজ্ঞায়িত করে। এই মেথডটি আপনার সংরক্ষণ করতে চাওয়া স্টেট ধারণকারী একটি Bundle রিটার্ন করে। যখন SavedStateHandle তার স্টেট সংরক্ষণ করার জন্য প্রস্তুত হয়, তখন এটি SavedStateProvider থেকে Bundle পুনরুদ্ধার করতে saveState() কল করে এবং সংশ্লিষ্ট কী-এর জন্য Bundle সংরক্ষণ করে।

এমন একটি অ্যাপের উদাহরণ বিবেচনা করুন যা ACTION_IMAGE_CAPTURE ইন্টেন্টের মাধ্যমে ক্যামেরা অ্যাপ থেকে একটি ছবির অনুরোধ করে এবং ছবিটি সংরক্ষণের জন্য একটি টেম্পোরারি ফাইলও পাস করে। TempFileViewModel সেই টেম্পোরারি ফাইলটি তৈরির লজিককে এনক্যাপসুলেট করে।

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

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

private fun File.saveTempFile() = bundleOf("path", absolutePath)

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

ব্যবহারকারী ফিরে এলে File ডেটা পুনরুদ্ধার করতে, SavedStateHandle থেকে temp_file Bundle নিন। এটি saveTempFile() দ্বারা প্রদত্ত সেই একই Bundle , যাতে অ্যাবসোলিউট পাথ থাকে। এরপর সেই অ্যাবসোলিউট পাথ ব্যবহার করে একটি নতুন File ইনস্ট্যানশিয়েট করা যেতে পারে।

private fun File.saveTempFile() = bundleOf("path", absolutePath)

private fun Bundle.restoreTempFile() = if (containsKey("path")) {
    File(getString("path"))
} else {
    null
}

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        val tempFileBundle = savedStateHandle.get<Bundle>("temp_file")
        if (tempFileBundle != null) {
            tempFile = tempFileBundle.restoreTempFile()
        }
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
      return tempFile ?: File.createTempFile("temp", null).also {
          tempFile = it
      }
    }
}

টেস্টে সেভডস্টেটহ্যান্ডেল

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

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

অতিরিক্ত সম্পদ

ViewModel এর সেভড স্টেট মডিউল সম্পর্কে আরও তথ্যের জন্য, নিম্নলিখিত রিসোর্সগুলো দেখুন।

কোডল্যাবস

বিষয়বস্তু দেখুন

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