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