واحد «وضعیت ذخیره‌شده» برای ViewModel   بخشی از Android Jetpack.

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

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

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

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

راه‌اندازی

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

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

سپس می‌توانید نمونه‌ای از ViewModel را بدون پیکربندی اضافی در عناصر ترکیبی‌تان بازیابی کنید. کارخانه پیش‌فرض ViewModel SavedStateHandle مناسب را برای ViewModel شما فراهم می‌کند.

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 که در 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» شما می‌تواند وضعیت خود را پس‌از بازسازی به‌طور یکپارچه بازیابی کند.

برای استفاده از آن، کلاس داده‌تان را با @Serializable حاشیه‌نویسی کنید و از نماینده 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 به APIهای Saver در Compose متکی باشد، آرتیفکت lifecycle-viewmodel-compose نماینده saveable را ارائه می‌دهد. این کار امکان هم‌کنش‌پذیری بین SavedStateHandle و Saver «نوشتن» را فراهم می‌کند تا هر 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 برای برنامه‌تان.

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

به‌طور پیش‌فرض، می‌توانید در SavedStateHandle برای همان انواع داده‌ای که Bundle دارد با 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 Kotlin یا پیاده‌سازی مستقیم 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

محتوا را می‌بیند