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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

على عكس الحالة المحفوظة، يتم إتلاف ViewModels أثناء عملية إيقاف نهائي يبدأها النظام. لإعادة تحميل البيانات بعد إيقاف العملية نهائيًا التي بدأها النظام في ViewModel، استخدِم واجهة برمجة التطبيقات SavedStateHandle. بدلاً من ذلك، إذا كانت البيانات مرتبطة بواجهة المستخدم ولا تحتاج إلى الاحتفاظ بها في ViewModel، استخدِم rememberSerializable. بالنسبة إلى أنواع البيانات الأساسية أو الحالات التي لا تريد فيها استخدام @Serializable، استخدِم rememberSaveable. إذا كانت البيانات بيانات تطبيق، قد يكون من الأفضل حفظها على القرص.

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

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

تخزِّن واجهات برمجة التطبيقات، مثل rememberSerializable وrememberSaveable في Compose وSavedStateHandle في ViewModels، البيانات اللازمة لإعادة تحميل حالة واجهة المستخدم إذا أوقف النظام أحد المكوّنات وأعاد إنشائه لاحقًا. للتعامل مع بنى البيانات المعقّدة بكفاءة أكبر، تتيح مكتبة SavedStateHandle استخدام Kotlinx Serialization من خلال إضافة saved {}، ما يتيح لك حفظ واستعادة الكائنات الآمنة من حيث النوع بسلاسة إلى جانب الأنواع الأساسية العادية. لمعرفة كيفية تنفيذ الحالة المحفوظة باستخدام rememberSaveable، راجِع الحالة وJetpack Compose.

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

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

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

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

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

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

الوصول إلى الحالة المحفوظة باستخدام SavedStateRegistry

بدءًا من Fragment 1.1.0 أو الاعتمادية المتعدية Activity 1.0.0، تنفّذ مكوّنات واجهة المستخدم، مثل ComponentActivity، الواجهة SavedStateRegistryOwner وتوفّر SavedStateRegistry مرتبطًا بهذا المكوِّن. تسمح السمة SavedStateRegistry للمكوّنات بالربط بحالتك المحفوظة من أجل استخدامها أو المساهمة فيها. على سبيل المثال، يستخدم وحدة Saved State لـ ViewModel SavedStateRegistry لإنشاء SavedStateHandle وتوفيره لعناصر ViewModel. يمكنك استرداد SavedStateRegistry من داخل مالك مراحل النشاط عن طريق استدعاء savedStateRegistry.

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

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

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

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

  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 SearchActivity : ComponentActivity() {
    private var searchManager = SearchManager(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Set up your Compose UI here
        setContent {
            // ...
        }
    }
  }

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

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

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

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

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

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

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

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

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

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

استعادة الحالات المعقّدة: إعادة تجميع الأجزاء

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

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

مراجع إضافية

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

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

مشاهدة المحتوى