وحدة "الحالة المحفوظة" في ViewModel   وهي جزء من Android Jetpack.

كما هو موضّح في حفظ حالات واجهة المستخدم، يمكن لكائنات ViewModel التعامل مع تغييرات الإعدادات، لذا لا داعي للقلق بشأن الحالة في حالات التدوير أو الحالات الأخرى. ومع ذلك، إذا كنت بحاجة إلى التعامل مع إيقاف العملية الذي بدأه النظام، قد يكون من المفيد استخدام واجهة برمجة التطبيقات SavedStateHandle كنسخة احتياطية.

عادةً ما يتم تخزين حالة واجهة المستخدم أو الإشارة إليها في كائنات 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، يمكنك تفعيل استخدام SavedStateHandle من خلال استخدام CreationExtras ولغة النطاق الخاصة بـ viewModelFactory.

استخدام 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 Serialization

بالنسبة إلى حالة واجهة المستخدم المعقّدة، يمكنك استخدام المفوّض الخاص بالسمة saved إلى جانب KotlinX Serialization. يتيح لك هذا المفوّض الاحتفاظ بفئات البيانات المخصّصة @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)
    }
}

دعم حالة Compose

إذا كانت حالتك تعتمد على واجهات برمجة التطبيقات Saver في Compose بدلاً من KotlinX Serialization، يوفّر العنصر 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+)

إذا لم تكن الفئة تندرج ضمن إحدى الفئات في القائمة أعلاه، ننصحك بجعل الـ فئة قابلة للتجزئة عن طريق إضافة الـ @Parcelize في Kotlin أو تنفيذ الـ Parcelable مباشرةً.

حفظ الفئات غير القابلة للتجزئة

إذا لم تنفّذ فئة Parcelable أو Serializable ولا يمكن تعديلها لتنفيذ إحدى هاتَين الواجهتَين، لن يكون من الممكن حفظ نسخة من هذه الفئة مباشرةً في SavedStateHandle.

بدءًا من الإصدار 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 عندما يعود المستخدم، استردِ Bundle الخاص بـ temp_file من 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، يُرجى الاطّلاع على المراجع التالية.

اختبارات الرموز

عرض المحتوى