ماژول وضعیت ذخیره شده برای ViewModel بخشی از Android Jetpack .

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

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

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

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

راه‌اندازی

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

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

سپس می‌توانید بدون هیچ پیکربندی اضافی، نمونه‌ای از ViewModel خود را در composables خود بازیابی کنید. کارخانه پیش‌فرض 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 با استفاده از یک نگهدارنده داده قابل مشاهده بازیابی کنید. لیست انواع پشتیبانی شده شامل موارد زیر است:

جریان حالت

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

  • 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 استفاده کنید. این نماینده به شما امکان می‌دهد کلاس‌های داده سفارشی @Serializable را مستقیماً در SavedStateHandle ذخیره کنید. این امر وضعیت ViewModel شما را در طول مرگ فرآیند حفظ می‌کند، بنابراین رابط کاربری Compose شما می‌تواند پس از بازسازی، وضعیت خود را به طور یکپارچه بازیابی کند.

برای استفاده از آن، کلاس داده خود را با @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)
    }
}

پشتیبانی ایالتی را بنویسید

اگر وضعیت شما به جای سریال‌سازی KotlinX به APIهای 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 نگهداری می‌شوند، به همراه بقیه‌ی savedInstanceState برای برنامه‌ی شما، به عنوان یک Bundle ذخیره و بازیابی می‌شوند.

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

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

اگر کلاس از هیچ یک از موارد ذکر شده در لیست بالا ارث‌بری نمی‌کند، می‌توانید با اضافه کردن حاشیه‌نویسی @Parcelize کاتلین یا پیاده‌سازی مستقیم Parcelable کلاس را 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)
    }
}

منابع اضافی

برای اطلاعات بیشتر در مورد ماژول Saved State برای ViewModel ، به منابع زیر مراجعه کنید.

کدلبز

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

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}