حفظ حالات واجهة المستخدم

يناقش هذا الدليل توقعات المستخدم بشأن حالة واجهة المستخدم، والخيارات المتاحة للحفاظ على الحالة.

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

لسد الفجوة بين توقعات المستخدم وسلوك النظام، استخدِم مجموعة من الطرق التالية:

  • ViewModel.
  • حالات المثيلات المحفوظة ضمن السياقات التالية:
  • مساحة تخزين محلية للحفاظ على حالة واجهة المستخدم أثناء عمليات نقل التطبيقات والأنشطة.

يعتمد الحل الأمثل على مدى تعقيد بيانات واجهة المستخدم وحالات استخدام التطبيق وإيجاد التوازن بين سرعة الوصول إلى البيانات واستخدام الذاكرة.

تأكد من أن تطبيقك يلبي توقعات المستخدمين ويوفر واجهة سريعة وسريعة الاستجابة. تجنَّب التأخيرات عند تحميل البيانات إلى واجهة المستخدم، وخاصةً بعد إجراء تغييرات الإعدادات الشائعة مثل التناوب.

توقعات المستخدم وسلوك النظام

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

إغلاق حالة واجهة المستخدم التي يبدأها المستخدم

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

  • تمرير النشاط سريعًا خارج شاشة "نظرة عامة" (الأخيرة).
  • إيقاف التطبيق أو إغلاقه من خلال شاشة "الإعدادات"
  • جارٍ إعادة تشغيل الجهاز.
  • جارٍ إكمال بعض إجراءات "الإنهاء" (المستندة إلى Activity.finish()).

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

هناك بعض الاستثناءات لهذه القاعدة المتعلّقة بالرفض الكامل، فعلى سبيل المثال، قد يتوقع المستخدم أن يوجّه المتصفّح المستخدم إلى صفحة الويب نفسها التي كان يبحث عنها قبل خروجه من المتصفّح باستخدام زر الرجوع.

إغلاق حالة واجهة المستخدم التي بدأها النظام

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

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

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

خيارات الحفاظ على حالة واجهة المستخدم

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

يختلف كل خيار من الخيارات للحفاظ على حالة واجهة المستخدم وفقًا للأبعاد التالية التي تؤثر في تجربة المستخدم:

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

استخدام ViewModel للتعامل مع تغييرات الإعدادات

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

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

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

على عكس حالة المثيل المحفوظ، يتم تدمير ViewModels أثناء إنهاء العملية التي يبدأها النظام. لإعادة تحميل البيانات بعد انتهاء العملية التي بدأها النظام في ViewModel، استخدِم واجهة برمجة تطبيقات SavedStateHandle. بدلاً من ذلك، إذا كانت البيانات مرتبطة بواجهة المستخدم ولا تحتاج إلى الاحتفاظ بها في ViewModel، استخدِم onSaveInstanceState() في نظام "View" أو "rememberSaveable" في Jetpack Compose. إذا كانت البيانات هي بيانات التطبيق، قد يكون من الأفضل الاحتفاظ بها على القرص.

إذا كان لديك حلّ في الذاكرة لتخزين حالة واجهة المستخدم بالاستناد إلى تغييرات الإعدادات، قد لا تحتاج إلى استخدام ViewModel.

استخدام حالة المثيل المحفوظ كنسخة احتياطية للتعامل مع إيقاف العملية الذي يبدأه النظام

يخزِّن معاودة الاتصال onSaveInstanceState() في "نظام العرض" وrememberSaveable في Jetpack Compose وSavedStateHandle في ViewModels البيانات اللازمة لإعادة تحميل حالة وحدة التحكّم في واجهة المستخدم، مثل نشاط أو جزء، في حال تدمير وحدة التحكّم تلك وأعاد إنشائها لاحقًا. لمعرفة كيفية تنفيذ حالة المثيل المحفوظ باستخدام onSaveInstanceState، يمكنك الاطّلاع على حفظ حالة النشاط واستعادتها في دليل مراحل نشاط النشاط.

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

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

استنادًا إلى حالات استخدام تطبيقك، قد لا تحتاج إلى استخدام حالة المثيل المحفوظة على الإطلاق. على سبيل المثال، قد يعيد المتصفح المستخدم إلى صفحة الويب الدقيقة التي كان يبحث عنها قبل خروجه من المتصفح. إذا كان نشاطك يتصرف بهذه الطريقة، يمكنك التخلي عن استخدام حالة المثيل المحفوظ وبدلاً من ذلك الاحتفاظ بكل شيء محليًا.

بالإضافة إلى ذلك، عند فتح نشاط من غرض معيّن، يتم تسليم مجموعة من الإجراءات الإضافية إلى النشاط، وذلك عند تغيير الإعدادات وعند استعادة النظام للنشاط. إذا تم تمرير جزء من بيانات حالة واجهة المستخدم، مثل طلب بحث، كهدف إضافي عند بدء النشاط، يمكنك استخدام حزمة العناصر الإضافية بدلاً من حزمة حالة المثيل المحفوظة. ولمزيد من المعلومات عن العناصر الإضافية، اطّلِع على فلاتر Intent وIntent.

في أي من هذه السيناريوهات، عليك مواصلة استخدام ViewModel لتجنُّب إهدار الدورات المتكررة لإعادة تحميل البيانات من قاعدة البيانات أثناء إجراء تغيير على الإعدادات.

في الحالات التي تكون فيها بيانات واجهة المستخدم المطلوب الاحتفاظ بها بسيطة وسهلة، يمكنك استخدام واجهات برمجة التطبيقات للحالة المحفوظة فقط للحفاظ على بيانات الحالة.

تسجيل حالة الحفظ باستخدام واجهة SaveStateRegistry

بدءًا من Fragment 1.1.0 أو تبعية هذا الجزء Activity1.0.0، يجب على وحدات التحكم في واجهة المستخدم، مثل Activity أو Fragment، تنفيذ SavedStateRegistryOwner وتقديم SavedStateRegistry المرتبطة بوحدة التحكّم هذه. يسمح SavedStateRegistry للمكونات بالربط بالحالة المحفوظة لوحدة تحكم واجهة المستخدم لاستهلاكها أو المساهمة فيها. على سبيل المثال، تستخدم وحدة الحالة المحفوظة لـ ViewModel SavedStateRegistry لإنشاء SavedStateHandle وتقديمها إلى عناصر ViewModel. يمكنك استرداد SavedStateRegistry من داخل وحدة التحكم في واجهة المستخدم عن طريق طلب getSavedStateRegistry().

يجب على المكونات التي تساهم في الحالة المحفوظة تنفيذ SavedStateRegistry.SavedStateProvider، والتي تحدد طريقة واحدة تُعرف باسم saveState(). تسمح الطريقة saveState() للمكون بعرض Bundle يحتوي على أي حالة يجب حفظها من هذا المكوِّن. يستدعي SavedStateRegistry هذه الطريقة أثناء مرحلة حالة الحفظ لدورة حياة وحدة تحكم واجهة المستخدم.

Kotlin

class SearchManager : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val QUERY = "query"
    }

    private val query: String? = null

    ...

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

لتسجيل SavedStateProvider، يُرجى الاتصال بـ registerSavedStateProvider() على SavedStateRegistry، مع إدخال مفتاح لربطه ببيانات مقدّم الخدمة بالإضافة إلى مقدّم الخدمة. يمكن استرداد البيانات المحفوظة سابقًا لمقدّم الخدمة من الحالة المحفوظة من خلال طلب الرقم consumeRestoredStateForKey() في SavedStateRegistry، وتمرير المفتاح المرتبط ببيانات مقدّم الخدمة.

ضمن Activity أو Fragment، يمكنك تسجيل SavedStateProvider في onCreate() بعد طلب الرقم super.onCreate(). وبدلاً من ذلك، يمكنك ضبط السمة LifecycleObserver على SavedStateRegistryOwner التي تنفّذ LifecycleOwner، وتسجيل SavedStateProvider بعد فعاليةON_CREATE. وباستخدام LifecycleObserver، يمكنك فصل التسجيل واسترداد الحالة المحفوظة سابقًا من SavedStateRegistryOwner نفسها.

Kotlin

class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val PROVIDER = "search_manager"
        private const val QUERY = "query"
    }

    private val query: String? = null

    init {
        // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
        registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_CREATE) {
                val registry = registryOwner.savedStateRegistry

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this)

                // Get the previously saved state and restore it
                val state = registry.consumeRestoredStateForKey(PROVIDER)

                // Apply the previously saved state
                query = state?.getString(QUERY)
            }
        }
    }

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }

    ...
}

class SearchFragment : Fragment() {
    private var searchManager = SearchManager(this)
    ...
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this);

                // Get the previously saved state and restore it
                Bundle state = registry.consumeRestoredStateForKey(PROVIDER);

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

استخدم المثابرة المحلية للتعامل مع إيقاف العملية للبيانات المعقدة أو الكبيرة

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

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

إدارة حالة واجهة المستخدم: القسمة والانتصار

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

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

كمثال، ضع في اعتبارك نشاطًا يتيح لك البحث في مكتبة الأغاني الخاصة بك. في ما يلي كيفية التعامل مع الأحداث المختلفة:

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

عندما يبحث المستخدم عن أغنية، مهما كانت بيانات الأغنية المعقّدة التي تحمّلها من قاعدة البيانات، يجب تخزينها مباشرةً في العنصر ViewModel كجزء من حالة واجهة مستخدم الشاشة.

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

استعادة الحالات المعقّدة: إعادة تجميع القطع

عندما يحين وقت عودة المستخدم إلى النشاط، هناك سيناريوهان محتملان لإعادة إنشاء النشاط:

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

مراجع إضافية

لمزيد من المعلومات حول حفظ حالات واجهة المستخدم، راجع الموارد التالية.

المدوّنات