Saved State module for ViewModel   Part of Android Jetpack.

همان‌طور که در ذخیره کردن حالت‌های میانای کاربر ذکر شد، اشیای ViewModel می‌توانند تغییرات پیکربندی را مدیریت کنند، بنابراین لازم نیست نگران حالت در چرخش‌ها یا موارد دیگر باشید. بااین‌حال، اگر لازم است مرگ پردازش آغازشده توسط سیستم را مدیریت کنید، شاید بخواهید از SavedStateHandle API به‌عنوان پشتیبان استفاده کنید.

وضعیت میانای کاربر معمولاً در اشیاء ViewModel ذخیره یا ارجاع داده می‌شود، بنابراین استفاده از rememberSaveable در Compose به مقداری کد تکراری نیاز دارد که واحد وضعیت ذخیره‌شده می‌تواند آن را برای شما مدیریت کند.

هنگام استفاده از این ماژول، اشیاء ViewModel یک شیء SavedStateHandle را ازطریق سازنده آن دریافت می‌کنند. این شیء یک نقشه کلید-مقدار است که به شما امکان می‌دهد اشیاء را در وضعیت ذخیره‌شده بنویسید و بازیابی کنید. این مقادیر پس‌از توقف فرایند توسط سیستم باقی می‌مانند و ازطریق همان شیء دردسترس خواهند بود.

وضعیت ذخیره‌شده به پشته تکلیف شما گره خورده است. اگر پشته تکلیف شما ازبین برود، وضعیت ذخیره‌شده شما نیز ازبین می‌رود. این اتفاق ممکن است هنگام توقف اجباری برنامه، برداشتن برنامه از منو موارد اخیر، یا بازراه‌اندازی دستگاه رخ دهد. دراین‌صورت، پشته تکلیف ناپدید می‌شود و نمی‌توانید اطلاعات را در حالت ذخیره‌شده بازیابی کنید. در سناریوهای رد کردن وضعیت میانای کاربر، وضعیت ذخیره‌شده بازیابی نمی‌شود. در سناریوهای راه‌اندازی‌شده توسط سیستم، بله.

راه‌اندازی

برای استفاده از SavedStateHandle، آن را به‌عنوان آرگومان سازنده برای ViewModel خود بپذیرید.

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

سپس می‌توانید نمونه‌ای از ViewModel را در composable خود بدون هیچ پیکربندی اضافی بازیابی کنید. کارخانه پیش‌فرض ViewModel، SavedStateHandle مناسب را برای ViewModel شما ارائه می‌دهد.

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

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

هنگام ارائه نمونه ViewModelProvider.Factory سفارشی، می‌توانید استفاده از SavedStateHandle را بااستفاده از CreationExtras و viewModelFactory DSL فعال کنید.

کار کردن با SavedStateHandle

کلاس SavedStateHandle یک نقشه کلید-مقدار است که به شما امکان می‌دهد داده‌ها را ازطریق روش‌های set() و get() در وضعیت ذخیره‌شده بنویسید و بازیابی کنید.

بااستفاده از SavedStateHandle، مقدار پُرسمان درطول مرگ پردازش حفظ می‌شود و مطمئن می‌شوید که کاربر همان مجموعه داده‌های فیلترشده را قبل و بعداز بازآفرینی می‌بیند، بدون اینکه فعالیت یا قطعه نیاز به ذخیره، بازیابی، و ارسال دستی آن مقدار به ViewModel داشته باشد.

SavedStateHandle همچنین روش‌های دیگری دارد که ممکن است هنگام تعامل با نقشه مقدار کلید انتظار داشته باشید:

  • contains(String key) - بررسی می‌کند که آیا مقداری برای کلید داده‌شده وجود دارد یا نه.
  • remove(String key) - مقدار کلید داده‌شده را برمی‌دارد.
  • keys() - همه کلیدهای موجود در SavedStateHandle را برمی‌گرداند.

علاوه‌براین، می‌توانید مقادیر را از SavedStateHandle بااستفاده از نگهدارنده داده‌های دیده‌شدنی بازیابی کنید. فهرست انواع پشتیبانی‌شده شامل موارد زیر است:

StateFlow

می‌توانید مقادیر را از SavedStateHandle که در یک مشاهده‌پذیر StateFlow پیچیده شده است بازیابی کنید. بسته به اینکه آیا نیاز به تغییر مستقیم مقدار دارید یا نه، می‌توانید بین یک جریان فقط خواندنی یا قابل تغییر انتخاب کنید:

  • getStateFlow(): اگر فقط نیاز به خواندن وضعیت دارید، از این مورد استفاده کنید. وقتی مقدار کلید را در جای دیگری در SavedStateHandle به‌روزرسانی می‌کنید، StateFlow مقدار جدید را دریافت می‌کند. این حالت زمانی ایده‌آل است که بخواهید یک جاری‌سازی فقط خواندنی را نمایش دهید و آن را بااستفاده از عملگرهای Flow تبدیل کنید.
  • ‫getMutableStateFlow(): اگر به دسترسی خواندن و نوشتن نیاز دارید، از این گزینه استفاده کنید. به‌روزرسانی .value MutableStateFlow برگردانده‌شده به‌طور خودکار 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

برای وضعیت پیچیده واسط کاربر، می‌توانید از نماینده ویژگی saved در کنار KotlinX Serialization استفاده کنید. این نماینده به شما امکان می‌دهد کلاس‌های داده @Serializable سفارشی را مستقیماً در SavedStateHandle حفظ کنید. این کار وضعیت ViewModel شما را درطول مرگ پردازش حفظ می‌کند، بنابراین Compose UI شما می‌تواند به‌طور یکپارچه وضعیت خود را پس‌از بازآفرینی بازیابی کند.

برای استفاده از آن، کلاس داده‌تان را با @Serializable حاشیه‌نویسی کنید و از نماینده saved در ViewModel خود استفاده کنید:

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 به میاناهای برنامه‌سازی کاربردی Saver Compose متکی است، آرتیفکت lifecycle-viewmodel-compose نماینده saveable را ارائه می‌دهد. این امر امکان هم‌کنش‌پذیری بین SavedStateHandle و Saver در Compose را فراهم می‌کند تا هر State که می‌توانید ازطریق rememberSaveable با Saver سفارشی ذخیره کنید، بتواند با 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 برای برنامه‌تان.

انواع مستقیماً پشتیبانی‌شده

به‌طور پیش‌فرض، می‌توانید set() و get() را در SavedStateHandle برای همان انواع داده 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+)

اگر کلاس یکی از کلاس‌های فهرست بالا را گسترش نمی‌دهد، با افزودن حاشیه‌نویسی Kotlin @Parcelize یا پیاده‌سازی مستقیم Parcelable، کلاس را قابل‌بسته‌بندی کنید.

ذخیره کردن کلاس‌های غیرقابل‌تجزیه

اگر کلاسی Parcelable یا Serializable را پیاده‌سازی نکند و نتوان آن را برای پیاده‌سازی یکی از این رابط‌ها اصلاح کرد، امکان ذخیره مستقیم نمونه‌ای از آن کلاس در SavedStateHandle وجود ندارد.

با شروع از Lifecycle 2.3.0-alpha03، SavedStateHandle به شما امکان می‌دهد هر شیئی را با ارائه منطق خودتان برای ذخیره و بازیابی شیء به‌عنوان Bundle بااستفاده از روش setSavedStateProvider() ذخیره کنید. ‫SavedStateRegistry.SavedStateProvider میانایی است که یک روش saveState() را تعریف می‌کند که Bundle حاوی وضعیتی را که می‌خواهید ذخیره کنید برمی‌گرداند. وقتی SavedStateHandle آماده ذخیره کردن وضعیتش است، saveState() را برای بازیابی Bundle از SavedStateProvider فراخوانی می‌کند و 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 هنگام بازگشت کاربر، temp_file Bundle را از SavedStateHandle بازیابی کنید. این همان 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 در آزمایش‌ها

برای آزمایش 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، به منابع زیر مراجعه کنید.

Codelabs

محتوا را مشاهده می‌کند