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