ViewModel-এর জন্য সেভ করা স্টেট মডিউল   Android Jetpack-এর অংশ।

UI স্টেট সেভ করা নিবন্ধে উল্লেখ করা হয়েছে যে, 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

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

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

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

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 Serialization সহায়তা

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

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

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

Compose স্টেট সংক্রান্ত সহায়তা

আপনার স্টেট যদি KotlinX Serialization-এর পরিবর্তে Compose-এর Saver API-এর উপর নির্ভর করে, তাহলে lifecycle-viewmodel-compose আর্টিফ্যাক্টটি saveable ডেলিগেট প্রদান করে। এর ফলে SavedStateHandle ও Compose-এর Saver-এর মধ্যে ইন্টারঅপারেবিলিটি নিশ্চিত করা যায়, যাতে কাস্টম Saver সহ rememberSaveable-এর মাধ্যমে সেভ করা যায় এমন যেকোনও 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-এর মধ্যে থাকা ডেটা Bundle হিসেবে সেভ ও রিস্টোর করা হয়, আপনার অ্যাপের বাকি savedInstanceState-এর সাথে।

সরাসরি কাজ করে এমন ধরন

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

টাইপ/ক্লাস সাপোর্ট অ্যারে সংক্রান্ত সহায়তা
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 Kotlin অ্যানোটেশন যোগ করে অথবা সরাসরি Parcelable প্রয়োগ করে ক্লাসটিকে পার্সেলযোগ্য করে তোলার কথা বিবেচনা করুন।

পার্সেল করা যায় না এমন ক্লাস সেভ করা

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

Lifecycle 2.3.0-alpha03 থেকে শুরু করে, SavedStateHandle আপনাকে যেকোনও অবজেক্ট সেভ করতে দেয়। এর জন্য আপনাকে setSavedStateProvider() মেথড ব্যবহার করে Bundle হিসেবে আপনার অবজেক্ট সেভ ও রিস্টোর করার জন্য নিজস্ব লজিক প্রদান করতে হবে। SavedStateRegistry.SavedStateProvider হল এমন একটি ইন্টারফেস যা একটি সিঙ্গেল saveState() মেথডকে ডিফাইন করে যা Bundle রিটার্ন করে যার মধ্যে আপনি সেভ করতে চান এমন স্টেট থাকে। SavedStateHandle তার স্টেট সেভ করার জন্য রেডি হলে, saveState()-কে কল করে SavedStateProvider থেকে Bundle রিট্রিভ করে এবং সংশ্লিষ্ট কী-এর জন্য 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 প্রয়োগ করুন এবং SavedStateHandle-এ এটিকে প্রদানকারী হিসেবে সেট করুন ViewModel:

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 রিট্রিভ করুন। এটি হল সেই একই Bundle যা saveTempFile() প্রদান করে এবং এতে অ্যাবসোলিউট পাথ থাকে। তারপরে, নতুন 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
      }
    }
}

টেস্টে SavedStateHandle

SavedStateHandle-এর উপর নির্ভরশীল ViewModel পরীক্ষা করতে, SavedStateHandle-এর একটি নতুন ইনস্ট্যান্স তৈরি করুন এবং এটিতে প্রয়োজনীয় টেস্ট ভ্যালু পাস করুন যে ViewModel ইনস্ট্যান্স পরীক্ষা করছেন তাতে।

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

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

অতিরিক্ত রিসোর্স

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

কোডল্যাবস

কন্টেন্ট দেখা