المفاهيم وتنفيذ Jetpack Compose
يناقش هذا الدليل توقّعات المستخدمين بشأن حالة واجهة المستخدم والخيارات المتاحة للاحتفاظ بالحالة.
إنّ حفظ حالة واجهة المستخدم لنشاط واستعادتها بسرعة بعد أن يدمّر النظام الأنشطة أو التطبيقات أمر ضروري لتوفير تجربة مستخدم جيدة. يتوقّع المستخدمون أن تظل حالة واجهة المستخدم كما هي، ولكن قد يمحو النظام النشاط والحالة المخزّنة له.
لسدّ الفجوة بين توقّعات المستخدمين وسلوك النظام، استخدِم مجموعة من الطرق التالية:
ViewModelobjects.- حالات المثيل المحفوظة ضمن السياقات التالية:
- طرق العرض:
onSaveInstanceState()واجهة برمجة التطبيقات - ViewModels:
SavedStateHandle
- طرق العرض:
- مساحة التخزين المحلية للاحتفاظ بحالة واجهة المستخدم أثناء عمليات الانتقال بين التطبيقات والأنشطة
يعتمد الحلّ الأمثل على مدى تعقيد بيانات واجهة المستخدم وحالات استخدام تطبيقك، وعلى إيجاد توازن بين سرعة الوصول إلى البيانات واستخدام الذاكرة.
تأكَّد من أنّ تطبيقك يلبي توقّعات المستخدمين ويوفر واجهة سريعة ومتجاوبة. تجنَّب التأخير عند تحميل البيانات في واجهة المستخدم، خاصةً بعد تغييرات الإعدادات الشائعة، مثل تدوير الشاشة.
توقّعات المستخدمين وسلوك النظام
استنادًا إلى الإجراء الذي يتّخذه المستخدم، يتوقّع إما محو حالة النشاط أو الاحتفاظ بها. في بعض الحالات، ينفّذ النظام تلقائيًا ما يتوقّعه المستخدم. وفي حالات أخرى، ينفّذ النظام العكس.
إغلاق حالة واجهة المستخدم من قِبل المستخدم
يتوقّع المستخدم أن تظل حالة واجهة المستخدم المؤقتة للنشاط كما هي عند بدء النشاط إلى أن يغلقه المستخدم تمامًا. يمكن للمستخدم إغلاق النشاط تمامًا من خلال إجراء ما يلي:
- تمرير النشاط سريعًا خارج شاشة "نظرة عامة" (التطبيقات الحديثة)
- إيقاف التطبيق أو إغلاقه بالقوة من شاشة "الإعدادات"
- إعادة تشغيل الجهاز
- إكمال نوع من إجراءات "الإنهاء" (التي تستند إلى
Activity.finish())
في حالات الإغلاق الكامل هذه، يفترض المستخدم أنّه انتقل نهائيًا من النشاط، وإذا أعاد فتح النشاط، يتوقّع أن يبدأ من حالة نظيفة. يتطابق سلوك النظام الأساسي في سيناريوهات الإغلاق هذه مع توقّعات المستخدم، إذ سيتم تدمير مثيل النشاط وإزالته من الذاكرة، بالإضافة إلى أي حالة مخزّنة فيه وأي سجلّ حالة مثيل محفوظ مرتبط بالنشاط.
هناك بعض الاستثناءات لهذه القاعدة بشأن الإغلاق الكامل، على سبيل المثال، قد يتوقّع المستخدم أن ينقله المتصفّح إلى صفحة الويب نفسها التي كان يطّلع عليها قبل الخروج من المتصفّح باستخدام زر الرجوع.
إغلاق حالة واجهة المستخدم من قِبل النظام
يتوقّع المستخدم أن تظل حالة واجهة المستخدم للنشاط كما هي طوال فترة تغيير الإعدادات، مثل تدوير الشاشة أو التبديل إلى وضع النوافذ المتعددة. ومع ذلك، يدمّر النظام النشاط تلقائيًا عند حدوث تغيير في الإعدادات، ما يؤدي إلى محو أي حالة لواجهة المستخدم مخزّنة في مثيل النشاط. لمعرفة المزيد عن إعدادات الجهاز، يُرجى الاطّلاع على صفحة مرجع الإعدادات.
ملاحظة: من الممكن (ولكن لا يُنصح بذلك) إلغاء السلوك التلقائي لتغييرات الإعدادات. لمزيد من التفاصيل، يُرجى الاطّلاع على مقالة معالجة تغيير الإعدادات.
يتوقّع المستخدم أيضًا أن تظل حالة واجهة المستخدم للنشاط كما هي إذا انتقل مؤقتًا إلى تطبيق مختلف ثم عاد إلى تطبيقك لاحقًا. على سبيل المثال، يُجري المستخدم عملية بحث في سجلّ البحث ثم يضغط على زر الشاشة الرئيسية أو يردّ على مكالمة هاتفية. وعندما يعود إلى سجلّ البحث، يتوقّع أن يجد كلمة بحث رئيسية والنتائج كما كانت من قبل.
في هذا السيناريو، يتم وضع تطبيقك في الخلفية، ويبذل النظام قصارى جهده للحفاظ على عملية تطبيقك في الذاكرة. ومع ذلك، قد يدمّر النظام عملية التطبيق أثناء تفاعل المستخدم مع تطبيقات أخرى. في هذه الحالة، يتم تدمير مثيل النشاط، بالإضافة إلى أي حالة مخزّنة فيه. عندما يعيد المستخدم تشغيل التطبيق، تكون حالة النشاط نظيفة بشكل غير متوقّع. لمزيد من المعلومات عن إيقاف العملية نهائيًا، يُرجى الاطّلاع على مقالة العمليات ومراحل نشاط التطبيق.
خيارات الاحتفاظ بحالة واجهة المستخدم
عندما لا تتطابق توقّعات المستخدم بشأن حالة واجهة المستخدم مع سلوك النظام التلقائي، عليك حفظ حالة واجهة المستخدم واستعادتها لضمان أن يكون التدمير الذي يبدأه النظام غير مرئي للمستخدم.
تختلف كل خيارات الاحتفاظ بحالة واجهة المستخدم حسب الجوانب التالية التي تؤثر في تجربة المستخدم:
ViewModel |
حالة المثيل المحفوظة |
مساحة التخزين الثابتة |
|
مكان التخزين |
التخزين في الذاكرة |
التخزين في الذاكرة |
على القرص أو الشبكة |
الاحتفاظ بالحالة بعد تغيير الإعدادات |
نعم |
نعم |
نعم |
الاحتفاظ بالحالة بعد إيقاف العملية نهائيًا من قِبل النظام |
لا |
نعم |
نعم |
الاحتفاظ بالحالة بعد إغلاق النشاط أو إنهاءه بالكامل من قِبل المستخدم (`finish()`) |
لا |
لا |
نعم |
الحدود القصوى للبيانات |
يمكن استخدام الكائنات المعقّدة، ولكن المساحة محدودة حسب الذاكرة المتاحة |
لأنواع البيانات الأساسية والكائنات البسيطة الصغيرة، مثل |
يقتصر على مساحة القرص أو تكلفة / وقت الاسترداد من مصدر الشبكة |
وقت القراءة/الكتابة |
سريع (الوصول إلى الذاكرة فقط) |
بطيء (يتطلّب التسلسل/إلغاء التسلسل) |
بطيء (يتطلّب الوصول إلى القرص أو إجراء معاملة على الشبكة) |
استخدام ViewModel لمعالجة تغييرات الإعدادات
يُعدّ ViewModel مثاليًا لتخزين البيانات المرتبطة بواجهة المستخدم وإدارتها أثناء استخدام المستخدم للتطبيق بنشاط. يسمح بالوصول السريع إلى بيانات واجهة المستخدم ويساعدك في تجنُّب إعادة جلب البيانات من الشبكة أو القرص أثناء تدوير الشاشة وتغيير حجم النافذة وتغييرات الإعدادات الأخرى الشائعة. لمعرفة كيفية تنفيذ a ViewModel، يُرجى الاطّلاع على دليل ViewModel.
يحتفظ ViewModel بالبيانات في الذاكرة، ما يعني أنّ استردادها أقل تكلفة من استرداد البيانات من القرص أو الشبكة. يرتبط ViewModel بنشاط (أو مالك دورة حياة آخر)، ويظل في الذاكرة أثناء تغيير الإعدادات، ويربط النظام تلقائيًا ViewModel بمثيل النشاط الجديد الناتج عن تغيير الإعدادات.
يدمّر النظام تلقائيًا ViewModels عندما يخرج المستخدم من النشاط أو الجزء أو إذا استدعيت finish()، ما يعني أنّه يتم محو الحالة كما يتوقّع المستخدم في هذه السيناريوهات.
على عكس حالة المثيل المحفوظة، يتم تدمير ViewModels أثناء إيقاف العملية نهائيًا من قِبل النظام. لإعادة تحميل البيانات بعد إيقاف العملية نهائيًا من قِبل النظام في
ViewModel، استخدِم SavedStateHandle واجهة برمجة التطبيقات. بدلاً من ذلك، إذا كانت البيانات مرتبطة بواجهة المستخدم ولا تحتاج إلى الاحتفاظ بها في ViewModel، استخدِم onSaveInstanceState(). إذا كانت البيانات بيانات التطبيق، قد يكون من الأفضل
الاحتفاظ بها على القرص.
إذا كان لديك حاليًا حلّ في الذاكرة لتخزين حالة واجهة المستخدم أثناء تغييرات الإعدادات، قد لا تحتاج إلى استخدام ViewModel.
استخدام حالة المثيل المحفوظة كنسخة احتياطية لمعالجة إيقاف العملية نهائيًا من قِبل النظام
تحفَظ دالة ردّ النداء onSaveInstanceState() في نظام العرض و
SavedStateHandle في ViewModels البيانات اللازمة لإعادة تحميل حالة
وحدة التحكّم في واجهة المستخدم، مثل نشاط أو جزء، إذا دمّر النظام وحدة التحكّم هذه وأعاد إنشاءها
لاحقًا. لمعرفة كيفية تنفيذ حالة المثيل المحفوظة
باستخدام onSaveInstanceState، يُرجى الاطّلاع على حفظ حالة النشاط واستعادتها في الـ
دليل دورة حياة النشاط.
تستمر حِزم حالة المثيل المحفوظة خلال تغييرات الإعدادات وإيقاف العملية نهائيًا، ولكنها محدودة حسب مساحة التخزين والسرعة، لأنّ واجهات برمجة التطبيقات المختلفة تسلسِل البيانات. يمكن أن يستهلك التسلسل الكثير من الذاكرة إذا كانت الكائنات التي يتم تسلسلها معقّدة. بما أنّ هذه العملية تحدث على سلسلة التعليمات الرئيسية أثناء تغيير الإعدادات، يمكن أن يؤدي التسلسل الذي يستغرق وقتًا طويلاً إلى فقدان الإطارات والتقطّع المرئي.
لا تستخدِم حالة المثيل المحفوظة لتخزين كميات كبيرة من البيانات، مثل الصور النقطية، أو هياكل البيانات المعقّدة التي تتطلّب تسلسلاً أو إلغاء تسلسل مطوّلاً. بدلاً من ذلك، خزِّن أنواع البيانات الأساسية والكائنات البسيطة الصغيرة، مثل String فقط. على هذا النحو، استخدِم حالة المثيل المحفوظة لتخزين الحد الأدنى من البيانات الضرورية، مثل رقم تعريف، لإعادة إنشاء البيانات اللازمة لاستعادة واجهة المستخدم إلى حالتها السابقة في حال تعذّر آليات الاحتفاظ الأخرى. يجب أن تنفّذ معظم التطبيقات هذه العملية لمعالجة إيقاف العملية نهائيًا من قِبل النظام.
استنادًا إلى حالات استخدام تطبيقك، قد لا تحتاج إلى استخدام حالة المثيل المحفوظة على الإطلاق. على سبيل المثال، قد يعيد المتصفّح المستخدم إلى صفحة الويب نفسها التي كان يطّلع عليها قبل الخروج من المتصفّح. إذا كان نشاطك يتصرّف بهذه الطريقة، يمكنك التوقف عن استخدام حالة المثيل المحفوظة والاحتفاظ بكل شيء محليًا بدلاً من ذلك.
بالإضافة إلى ذلك، عند فتح نشاط من هدف، يتم تسليم حزمة الإضافات إلى النشاط عند تغيير الإعدادات وعندما يستعيد النظام النشاط.
في أيّ من هذين السيناريوهَين، يجب أن تستخدم ViewModel لتجنُّب
إضاعة الدورات في إعادة تحميل البيانات من قاعدة البيانات أثناء تغيير الإعدادات.
في الحالات التي تكون فيها بيانات واجهة المستخدم التي يجب الاحتفاظ بها بسيطة وخفيفة، يمكنك استخدام واجهات برمجة التطبيقات لحالة المثيل المحفوظة وحدها للاحتفاظ ببيانات الحالة.
الربط بالحالة المحفوظة باستخدام `SavedStateRegistry`
بدءًا من 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 هذه الطريقة أثناء مرحلة حفظ الحالة في دورة حياة وحدة التحكّم في واجهة المستخدم.
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(). بدلاً من ذلك، يمكنك ضبط a
LifecycleObserver على a SavedStateRegistryOwner، الذي ينفّذ
LifecycleOwner، وتسجيل the SavedStateProvider عند وقوع the
ON_CREATE event. باستخدام 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 كجزء من حالة واجهة مستخدم الشاشة.
عندما ينتقل النشاط إلى الخلفية ويستدعي النظام واجهات برمجة التطبيقات لحالة المثيل المحفوظة، يجب تخزين طلب البحث في حالة المثيل المحفوظة، في حال أعاد النظام إنشاء العملية. بما أنّ المعلومات ضرورية لتحميل بيانات التطبيق المحفوظة في هذه الحالة، خزِّن طلب البحث في SavedStateHandle الخاص بـ ViewModel. هذه هي كل المعلومات التي تحتاج إليها لتحميل البيانات وإعادة واجهة المستخدم إلى حالتها الحالية.
استعادة الحالات المعقّدة: إعادة تجميع الأجزاء
عندما يحين وقت عودة المستخدم إلى النشاط، هناك سيناريوهان محتمَلان لإعادة إنشاء النشاط:
- تتم إعادة إنشاء النشاط بعد أن أوقفه النظام. يحتفظ النظام بالطلب المحفوظ في حزمة حالة مثيل محفوظ، ويجب أن تمرِّر واجهة المستخدم الطلب إلى
ViewModelإذا لم يتم استخدامSavedStateHandle. يرىViewModelأنّه لا يحتوي على أي نتائج بحث مخزّنة مؤقتًا ويفوّض تحميل نتائج البحث باستخدام طلب البحث المحدّد. - تتم إعادة إنشاء النشاط بعد تغيير الإعدادات. بما أنّه لم يتم تدمير مثيل
ViewModel، يحتويViewModelعلى جميع المعلومات المخزّنة مؤقتًا في الذاكرة ولا يحتاج إلى إعادة طلب قاعدة البيانات.
مراجع إضافية
لمزيد من المعلومات عن حفظ حالات واجهة المستخدم، يُرجى الاطّلاع على المراجع التالية.
المدوّنات
- ViewModels: مثال بسيط
- ViewModels: الاحتفاظ بالبيانات،
onSaveInstanceState، واستعادة حالة واجهة المستخدم وأدوات التحميل