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 অবজার্ভেবল-এ র‍্যাপ করা 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 সিরিয়ালাইজেশন সংক্রান্ত সহায়তা

জটিল 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)
    }
}

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

আপনার স্টেট 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 তার স্ট্যাটাস সেভ করার জন্য রেডি হলে, এটি 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
      }
    }
}

টেস্টে SavedStateHandle

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-এর জন্য সেভ করা স্টেট মডিউল সম্পর্কে আরও তথ্যের জন্য, নিম্নলিখিত রিসোর্সগুলি দেখুন।

কোডল্যাবস

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