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

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

عادةً ما يتم تخزين حالة واجهة المستخدم أو الإشارة إليها في كائنات ViewModel وليس في أنشطة، لذا فإن استخدام onSaveInstanceState() أو rememberSaveable يتطلب بعض النماذج النموذجية التي يمكن أن تعالجها وحدة الحالة المحفوظة.

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

ترتبط الحالة المحفوظة بحزمة المهام. إذا اختفت حزمة المهام الخاصة بك، فإن الحالة المحفوظة تختفي أيضًا. يمكن أن يحدث هذا عند فرض إيقاف أحد التطبيقات، أو إزالته من قائمة التطبيقات المستخدمة مؤخرًا، أو إعادة تشغيل الجهاز. وفي هذه الحالات، يختفي تكديس المهام ولن تتمكن من استعادة المعلومات في الحالة المحفوظة. في سيناريوهات إغلاق حالة واجهة المستخدم التي يبدأها المستخدم، لا تتم استعادة الحالة المحفوظة. في سيناريوهات يبدأها النظام، يكون الأمر كذلك.

ضبط إعدادات

بدءًا من Fragment 1.2.0 أو تبعيته المتبادلة النشاط 1.1.0، يمكنك قبول SavedStateHandle كوسيطة إنشائية لـ ViewModel.

Kotlin

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

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        state = savedStateHandle;
    }

    ...
}

يمكنك بعد ذلك استرداد مثيل ViewModel بدون أي إعدادات إضافية. يوفّر المصنع التلقائي لجهاز ViewModel SavedStateHandle المناسب لجهاز ViewModel.

Kotlin

class MainFragment : Fragment() {
    val vm: SavedStateViewModel by viewModels()

    ...
}

Java

class MainFragment extends Fragment {
    private SavedStateViewModel vm;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        vm = new ViewModelProvider(this).get(SavedStateViewModel.class);

        ...


    }

    ...
}

عند توفير مثيل ViewModelProvider.Factory مخصّص، يمكنك تفعيل استخدام SavedStateHandle من خلال توسيع نطاق AbstractSavedStateViewModelFactory.

العمل باستخدام SaveStateHandle

الفئة SavedStateHandle هي خريطة قيمة أساسية تتيح لك كتابة البيانات واستردادها من الحالة المحفوظة وإليها من خلال طريقتَي set() وget().

باستخدام SavedStateHandle، يتم الاحتفاظ بقيمة طلب البحث عند انتهاء عملية إيقاف العملية، لضمان ظهور مجموعة البيانات المفلتَرة نفسها للمستخدم قبل عملية الإنشاء وبعدها بدون إجراء أي نشاط أو جزء يحتاج إلى حفظ تلك القيمة واستعادتها وإعادة توجيهها يدويًا إلى ViewModel.

لدى SavedStateHandle أيضًا طرق أخرى قد تتوقعها عند التفاعل مع خريطة قيمة أساسية:

  • contains(String key) - للتحقّق من وجود قيمة للمفتاح المحدّد.
  • remove(String key) - لإزالة قيمة المفتاح المحدَّد.
  • keys() - لعرض جميع المفاتيح المضمّنة في SavedStateHandle.

بالإضافة إلى ذلك، يمكنك استرداد القيم من SavedStateHandle باستخدام صاحب بيانات يمكن ملاحظته. في ما يلي قائمة بالأنواع المتاحة:

بيانات مباشرة

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

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: LiveData<List<String>> =
        savedStateHandle.getLiveData<String>("query").switchMap { query ->
        repository.getFilteredData(query)
    }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle savedStateHandle;
    public LiveData<List<String>> filteredData;
    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        this.savedStateHandle = savedStateHandle;
        LiveData<String> queryLiveData = savedStateHandle.getLiveData("query");
        filteredData = Transformations.switchMap(queryLiveData, query -> {
            return repository.getFilteredData(query);
        });
    }

    public void setQuery(String query) {
        savedStateHandle.set("query", query);
    }
}

StateFlow

.

يمكنك استرداد القيم من SavedStateHandle التي تم تضمينها في StateFlow يمكن ملاحظتها باستخدام getStateFlow(). عند تعديل قيمة المفتاح، ستتلقّى StateFlow القيمة الجديدة. في معظم الأحيان، يمكنك ضبط القيمة بسبب تفاعلات المستخدم، مثل إدخال استعلام لتصفية قائمة البيانات. يمكنك بعد ذلك تحويل هذه القيمة المحدّثة باستخدام عوامل التدفق الأخرى.

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: StateFlow<List<String>> =
        savedStateHandle.getStateFlow<String>("query")
            .flatMapLatest { query ->
                repository.getFilteredData(query)
            }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

الدعم التجريبي لحالة Compose

يوفّر العنصر lifecycle-viewmodel-compose واجهات برمجة تطبيقات saveable التجريبية التي تسمح بإمكانية التشغيل التفاعلي بين SavedStateHandle وCompose Saver بحيث يمكن أيضًا حفظ أي State يمكنك حفظه من خلال rememberSaveable باستخدام Saver مخصّص باستخدام SavedStateHandle.

Kotlin

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 منطق إنشاء هذا الملف المؤقت.

Kotlin

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel() {
    }


    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }
}

لضمان عدم فقدان الملف المؤقت عند إنهاء عملية النشاط واستعادته لاحقًا، بإمكان TempFileViewModel استخدام SavedStateHandle للاحتفاظ ببياناته. للسماح لـ TempFileViewModel بحفظ بياناته، نفِّذ SavedStateProvider واضبطها كمقدّم خدمة في SavedStateHandle من ViewModel:

Kotlin

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
        }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        savedStateHandle.setSavedStateProvider("temp_file",
            new TempFileSavedStateProvider());
    }
    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }
    }
}

لاستعادة بيانات File عند عودة المستخدم، يمكنك استرداد temp_file Bundle من SavedStateHandle. هذه السمة هي السمة Bundle نفسها المقدَّمة من saveTempFile() والتي تحتوي على المسار المطلق. يمكن بعد ذلك استخدام المسار المطلق لإنشاء مثيل File جديد.

Kotlin

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
      }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        Bundle tempFileBundle = savedStateHandle.get("temp_file");
        if (tempFileBundle != null) {
            tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle);
        }
        savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider());
    }

    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }

        @Nullable
        private static File restoreTempFile(Bundle bundle) {
            if (bundle.containsKey("path") {
                return File(bundle.getString("path"));
            }
            return null;
        }
    }
}

SaveStateHandle في الاختبارات

لاختبار ViewModel الذي يأخذ SavedStateHandle كتبعية، أنشئ مثيل SavedStateHandle جديدًا بقيم الاختبار التي يتطلبها وقم بتمريره إلى مثيل ViewModel الذي تختبره.

Kotlin

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

مراجع إضافية

لمزيد من المعلومات حول وحدة "الحالة المحفوظة" لـ ViewModel، يُرجى الاطّلاع على المراجع التالية.

الدروس التطبيقية حول الترميز