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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

بدايةً من Fragment 1.1.0 أو اعتماديته المتبادلة Activity 1.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 كل المعلومات المخزَّنة مؤقتًا في الذاكرة ولا تحتاج إلى إعادة إجراء طلب بحث في قاعدة البيانات.

مراجع إضافية

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

المدوّنات