وحدة Saved State لـ 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 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 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 State
إذا كانت الحالة تعتمد على واجهات برمجة تطبيقات 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.
بدءًا من الإصدار 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، يُرجى الاطّلاع على
المراجع التالية.
اختبارات الرموز
محتوى المشاهدات
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- حفظ حالات واجهة المستخدم
- العمل مع عناصر البيانات القابلة للتتبّع
- إنشاء ViewModels باستخدام التبعيات