في Jetpack Compose، غالبًا ما تحتفظ الدوال القابلة للإنشاء بالحالة باستخدام الدالة remember. يمكن إعادة استخدام القيم التي يتم تذكّرها في عمليات إعادة الإنشاء، كما هو موضّح في State وJetpack Compose.
في حين أنّ remember تعمل كأداة للاحتفاظ بالقيم على مستوى عمليات إعادة الإنشاء، غالبًا ما تحتاج الحالة إلى البقاء نشطة بعد انتهاء مدة صلاحية عملية الإنشاء. توضّح هذه الصفحة الفرق بين واجهات برمجة التطبيقات remember وretain وrememberSaveable وrememberSerializable، ومتى يجب اختيار أي واجهة برمجة تطبيقات، وما هي أفضل الممارسات لإدارة القيم التي تم تذكّرها والاحتفاظ بها في Compose.
اختيار العمر المناسب
في Compose، تتوفّر عدة دوال يمكنك استخدامها للحفاظ على الحالة في عمليات الإنشاء وبعدها، وهي remember وretain وrememberSaveable وrememberSerializable. تختلف هذه الدوال في مدة بقائها ودلالاتها،
ويناسب كل منها تخزين أنواع معيّنة من الحالة. تم توضيح الاختلافات في الجدول التالي:
|
|
|
|
|---|---|---|---|
هل تبقى القيم بعد إعادة التركيب؟ |
✅ |
✅ |
✅ |
هل تظل القيم محفوظة عند إعادة إنشاء النشاط؟ |
❌ |
✅ سيتم دائمًا عرض المثيل نفسه ( |
✅ سيتم عرض عنصر مكافئ ( |
هل تظل القيم محفوظة عند إيقاف العملية نهائيًا؟ |
❌ |
❌ |
✅ |
أنواع البيانات المتوافقة |
الكل |
يجب عدم الإشارة إلى أي عناصر سيتم تسريبها في حال إيقاف النشاط |
يجب أن تكون قابلة للتسلسل |
حالات الاستخدام |
|
|
|
remember
remember هي الطريقة الأكثر شيوعًا لتخزين الحالة في Compose. عند استدعاء remember للمرة الأولى، يتم تنفيذ العملية الحسابية المحدّدة وتذكُّرها، ما يعني أنّ Compose يخزّنها لإعادة استخدامها في المستقبل من خلال العنصر القابل للإنشاء. عندما تتم إعادة تكوين دالة مركّبة، يتم تنفيذ رمزها مرة أخرى، ولكن أي استدعاءات إلى remember تعرض قيمها من عملية التكوين السابقة بدلاً من إعادة تنفيذ عملية الحساب.
تحتوي كل نسخة من دالة مركّبة على مجموعة خاصة من القيم التي تم تخزينها مؤقتًا، ويُشار إليها باسم التخزين المؤقت حسب الموضع. عندما يتم تخزين القيم التي تم تذكّرها مؤقتًا لاستخدامها في عمليات إعادة إنشاء متعددة، يتم ربطها بموضعها في التسلسل الهرمي للإنشاء. إذا تم استخدام عنصر قابل للإنشاء في مواقع مختلفة، سيكون لكل مثيل في التسلسل الهرمي للتركيب مجموعة خاصة به من القيم التي تم تذكُّرها.
عندما لا يتم استخدام قيمة محفوظة، يتم تجاهلها ويتم حذف سجلّها. يتم نسيان القيم التي تم تذكّرها عند إزالتها من التسلسل الهرمي للتكوين (بما في ذلك عند إزالة قيمة وإعادة إضافتها للانتقال إلى موقع مختلف بدون استخدام الدالة المركّبة key أو MovableContent)، أو عند استدعائها باستخدام مَعلمات key مختلفة.
من بين الخيارات المتاحة، remember هو الأقصر عمرًا وينسى القيم في أقرب وقت من بين دوال التخزين المؤقت الأربع الموضّحة في هذه الصفحة.
وهذا يجعلها الأنسب لما يلي:
- إنشاء عناصر الحالة الداخلية، مثل موضع التمرير أو حالة الصورة المتحركة
- تجنُّب إعادة إنشاء العناصر المكلفة في كل عملية إعادة إنشاء
ومع ذلك، عليك تجنُّب ما يلي:
- تخزين أي بيانات أدخلها المستخدم باستخدام
remember، لأنّه يتم نسيان العناصر التي تم تذكّرها عند إجراء تغييرات على إعدادات النشاط وعند إيقاف العملية التي بدأها النظام.
rememberSaveable وrememberSerializable
تستند الإصدارات rememberSaveable وrememberSerializable إلى الإصدار remember. وهي تتميّز بأطول عمر بين دوال التخزين المؤقت التي تمت مناقشتها في هذا الدليل.
بالإضافة إلى التخزين المؤقت للأغراض حسب الموضع في عمليات إعادة التكوين، يمكن أيضًا حفظ القيم ليتم استعادتها في عمليات إعادة إنشاء الأنشطة، بما في ذلك عمليات إعادة الإنشاء الناتجة عن تغييرات في الإعدادات وعن إيقاف العملية نهائيًا (عندما يوقف النظام عملية تطبيقك أثناء تشغيله في الخلفية، ويكون ذلك عادةً إما لتحرير الذاكرة للتطبيقات التي تعمل في المقدّمة أو إذا ألغى المستخدم الأذونات من تطبيقك أثناء تشغيله).
تعمل السمة rememberSerializable بالطريقة نفسها التي تعمل بها السمة rememberSaveable، ولكنّها تتيح تلقائيًا إمكانية الاحتفاظ بالأنواع المعقّدة التي يمكن تسلسلها باستخدام مكتبة kotlinx.serialization. اختَر rememberSerializable إذا كان نوعك
يحمل العلامة @Serializable (أو يمكن أن يحملها)، واختَر rememberSaveable في جميع الحالات الأخرى.
وهذا يجعل كلاً من rememberSaveable وrememberSerializable خيارَين مثاليَين لتخزين الحالة المرتبطة ببيانات أدخلها المستخدم، بما في ذلك إدخال حقل النص وموضع التمرير وحالات التبديل وغير ذلك. عليك حفظ هذه الحالة لضمان عدم فقدان المستخدم موضع التمرير. بشكل عام، يجب استخدام rememberSaveable أو rememberSerializable لتخزين أي حالة لا يمكن لتطبيقك استرجاعها من مصدر بيانات دائم آخر، مثل قاعدة بيانات.
يُرجى العِلم أنّ rememberSaveable وrememberSerializable يحفظان القيم المخزّنة مؤقتًا من خلال تحويلها إلى Bundle. وينتج عن ذلك نتيجتان:
- يجب أن تكون القيم التي يتم تخزينها مؤقتًا قابلة للتمثيل بنوع واحد أو أكثر من أنواع البيانات التالية: الأنواع الأساسية (بما في ذلك
IntأوLongأوFloatأوDouble) أوStringأو مصفوفات أي من هذه الأنواع. - عند استعادة قيمة محفوظة، ستكون مثيلاً جديدًا يساوي
(
==)، ولكن ليس المرجع نفسه (===) الذي استخدمته التركيبة من قبل.
لتخزين أنواع بيانات أكثر تعقيدًا بدون استخدام kotlinx.serialization، يمكنك تنفيذ Saver مخصّص لتسلسل العنصر وإلغاء تسلسله إلى أنواع البيانات المتوافقة. يُرجى العِلم أنّ Compose تفهم أنواع البيانات الشائعة، مثل State وList وMap وSet وما إلى ذلك، وتُحوِّلها تلقائيًا إلى أنواع متوافقة نيابةً عنك. في ما يلي مثال على
Saver لفئة Size. يتم تنفيذ ذلك من خلال تجميع جميع خصائص Size في قائمة باستخدام listSaver.
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
تتوفّر واجهة برمجة التطبيقات retain بين remember وrememberSaveable/rememberSerializable من حيث مدة تخزين القيم مؤقتًا. يتم تسميتها بشكل مختلف لأنّ القيم المحفوظة تمر أيضًا بدورة حياة مختلفة عن القيم التي تم تذكّرها.
عند الاحتفاظ بقيمة، يتم تخزينها مؤقتًا حسب الموضع وحفظها في بنية بيانات ثانوية لها مدة بقاء منفصلة مرتبطة بمدة بقاء التطبيق. يمكن أن تبقى القيمة المحفوظة بدون أن يتم تسلسلها عند إجراء تغييرات في الإعدادات، ولكن لا يمكن أن تبقى عند إيقاف العملية نهائيًا. إذا لم يتم استخدام قيمة بعد إعادة إنشاء التسلسل الهرمي للتركيب، يتم إيقاف القيمة المحتفظ بها (وهو ما يعادل نسيانها في retain).
في المقابل، يمكن لـ retain الاحتفاظ بالقيم التي لا يمكن تسلسلها، مثل تعبيرات lambda وعمليات نقل البيانات والكائنات الكبيرة، مثل الصور النقطية، وذلك مقابل دورة الحياة الأقصر من rememberSaveable. على سبيل المثال، يمكنك استخدام retain لإدارة مشغّل وسائط (مثل ExoPlayer) لمنع حدوث انقطاعات في تشغيل الوسائط أثناء تغيير الإعدادات.
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain مقابل ViewModel
في جوهرهما، يقدّم كل من retain وViewModel وظائف متشابهة في أكثر استخداماتهما شيوعًا، وهي إمكانية الاحتفاظ بنسخ الكائنات عند حدوث تغييرات في الإعدادات. يعتمد اختيار retain أو ViewModel على نوع القيمة التي تريد الاحتفاظ بها، وكيفية تحديد نطاقها، وما إذا كنت بحاجة إلى وظائف إضافية.
ViewModel هي عناصر تغلف عادةً عملية التواصل بين واجهة المستخدم وطبقات البيانات في تطبيقك. تتيح لك نقل منطق خارج الدوال القابلة للإنشاء، ما يحسّن إمكانية الاختبار. تتم إدارة ViewModel كعناصر فردية ضمن ViewModelStore، ولها مدة بقاء مختلفة عن القيم المحفوظة. سيظل ViewModel نشطًا إلى أن يتم إتلاف ViewModelStore، ولكن سيتم إيقاف القيم المحفوظة عند إزالة المحتوى نهائيًا من التركيب (على سبيل المثال، عند تغيير الإعدادات، يعني ذلك أنّه سيتم إيقاف القيمة المحفوظة إذا تمت إعادة إنشاء بنية واجهة المستخدم ولم يتم استخدام القيمة المحفوظة بعد إعادة إنشاء التركيب).
تتضمّن ViewModel أيضًا عمليات تكامل جاهزة للاستخدام لتوفير الاعتمادية مع Dagger وHilt، وعملية تكامل مع SavedState، وتوافقًا مدمجًا مع الكوروتين لتشغيل المهام في الخلفية. وهذا يجعل ViewModel مكانًا مثاليًا لتنفيذ مهام في الخلفية وطلبات الشبكة، والتفاعل مع مصادر البيانات الأخرى في مشروعك، والتقاط حالة واجهة المستخدم المهمة للغاية وحفظها بشكل اختياري، والتي يجب الاحتفاظ بها عند إجراء تغييرات في الإعدادات في ViewModel، كما يجب أن تنجو من إيقاف العملية نهائيًا.
يكون retain مناسبًا بشكل أفضل للعناصر التي يقتصر نطاقها على مثيلات معيّنة قابلة للإنشاء ولا تتطلّب إعادة استخدام أو مشاركة بين عناصر قابلة للإنشاء متجاورة. حيث إنّ ViewModel مكان مناسب لتخزين حالة واجهة المستخدم وتنفيذ المهام في الخلفية، فإنّ retain خيار مناسب لتخزين العناصر الخاصة ببنية واجهة المستخدم، مثل ذاكرات التخزين المؤقت وتتبُّع مرات الظهور والإحصاءات والتبعيات في AndroidView وغيرها من العناصر التي تتفاعل مع نظام التشغيل Android أو تدير المكتبات التابعة لجهات خارجية، مثل معالجات الدفع أو الإعلانات.
بالنسبة إلى المستخدمين المتقدّمين الذين يصمّمون أنماطًا مخصّصة لبنية التطبيقات خارج نطاق
اقتراحات بنية التطبيقات الحديثة على Android، يمكن أيضًا استخدام retain لإنشاء واجهة برمجة تطبيقات داخلية "تشبه ViewModel". على الرغم من أنّ دعم الروتينات الفرعية وحالة الحفظ غير متوفّرين بشكلٍ جاهز، يمكن استخدام retain كعنصر أساسي في مراحل نشاط العناصر المشابهة ViewModel التي تتضمّن هذه الميزات. لا يتناول هذا الدليل تفاصيل كيفية تصميم مثل هذا المكوّن.
|
|
|
|---|---|---|
تحديد النطاق |
لا توجد قيم مشترَكة، ويتم الاحتفاظ بكل قيمة وربطها بنقطة معيّنة في التسلسل الهرمي للتركيب. يؤدي الاحتفاظ بالنوع نفسه في موقع مختلف دائمًا إلى إنشاء مثيل جديد. |
|
التدمير |
عند مغادرة التسلسل الهرمي للتركيب نهائيًا |
عندما تتم إزالة |
وظائف إضافية |
يمكن تلقّي عمليات ردّ الاتصال عندما يكون العنصر في التسلسل الهرمي للتركيب أو لا يكون فيه |
يمكن إدخال |
المالك |
|
|
حالات الاستخدام |
|
|
الجمع بين retain وrememberSaveable أو rememberSerializable
في بعض الأحيان، يجب أن يكون لعنصر مدة صلاحية مختلطة من retained وrememberSaveable أو rememberSerializable. قد يشير ذلك إلى أنّ العنصر يجب أن يكون ViewModel، ما يتيح إمكانية حفظ الحالة كما هو موضّح في وحدة "الحالة المحفوظة" لدليل ViewModel.
يمكن استخدام retain وrememberSaveable أو rememberSerializable في الوقت نفسه. ويؤدي الجمع بين دورات الحياة هذه بشكل صحيح إلى زيادة التعقيد بشكل كبير.
ننصحك باستخدام هذا النمط كجزء من أنماط معمارية أكثر تقدّمًا ومخصّصة، وذلك فقط عندما تنطبق جميع الشروط التالية:
- تحديد عنصر يتألف من مجموعة من القيم التي يجب الاحتفاظ بها أو حفظها (مثل عنصر يتتبّع بيانات أدخلها المستخدم وذاكرة التخزين المؤقت التي لا يمكن الكتابة إليها على القرص)
- يكون نطاق حالتك محصورًا في دالة مركّبة ولا يكون مناسبًا لنطاق سينغلتون أو مدة بقاء
ViewModel
عندما تكون كل هذه الحالات صحيحة، ننصح بتقسيم صفك إلى ثلاثة أجزاء: البيانات المحفوظة والبيانات المحتفظ بها وعنصر "وسيط" ليس له حالة خاصة به ويفوّض الكائنَين المحفوظ والمحتفظ به لتعديل الحالة وفقًا لذلك. يتّخذ هذا النمط الشكل التالي:
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
من خلال فصل الحالة حسب مدة صلاحيتها، يصبح فصل المسئوليات والتخزين واضحًا جدًا. من المقصود ألا يمكن معالجة بيانات الحفظ من خلال الاحتفاظ بالبيانات، لأنّ ذلك يمنع حدوث سيناريو يتم فيه محاولة تعديل بيانات الحفظ عندما تكون حزمة savedInstanceState قد تم تسجيلها بالفعل ولا يمكن تعديلها. ويتيح أيضًا اختبار سيناريوهات إعادة الإنشاء من خلال اختبار أدوات الإنشاء بدون استدعاء Compose أو محاكاة إعادة إنشاء نشاط.
يمكنك الاطّلاع على النموذج الكامل (RetainAndSaveSample.kt) للحصول على مثال كامل حول كيفية تنفيذ هذا النمط.
التخزين المؤقت حسب الموضع والتنسيقات التكيّفية
يمكن أن تتوافق تطبيقات Android مع العديد من أشكال الأجهزة، بما في ذلك الهواتف والأجهزة القابلة للطي والأجهزة اللوحية وأجهزة الكمبيوتر المكتبي. تحتاج التطبيقات في كثير من الأحيان إلى الانتقال بين أشكال الأجهزة هذه باستخدام تخطيطات متجاوبة. على سبيل المثال، قد يتمكّن تطبيق يعمل على جهاز لوحي من عرض على شكل قائمة مع تفاصيل من عمودَين، ولكن قد يتنقّل بين قائمة وصفحة تفاصيل عند عرضه على شاشة هاتف أصغر.
بما أنّ القيم التي تم تذكّرها والاحتفاظ بها يتم تخزينها مؤقتًا حسب الموضع، لا تتم إعادة استخدامها إلا إذا ظهرت في الموضع نفسه في التسلسل الهرمي للتركيب. عندما تتكيّف التصاميم مع عوامل الشكل المختلفة، قد تغيّر بنية التسلسل الهرمي للتكوين وتؤدي إلى نسيان القيم.
بالنسبة إلى المكوّنات الجاهزة للاستخدام، مثل ListDetailPaneScaffold وNavDisplay
(من Jetpack Navigation 3)، لا يشكّل ذلك مشكلة وسيظل حالتك ثابتة
أثناء تغييرات التنسيق. بالنسبة إلى المكوّنات المخصّصة التي تتكيّف مع أشكال الأجهزة، تأكَّد من أنّ الحالة لا تتأثر بتغييرات التصميم من خلال تنفيذ أحد الإجراءات التالية:
- تأكَّد من أنّه يتم دائمًا استدعاء العناصر القابلة للإنشاء ذات الحالة في الموضع نفسه في التدرّج الهرمي للتكوين. يمكنك تنفيذ تنسيقات متجاوبة من خلال تغيير منطق التنسيق بدلاً من نقل العناصر في التسلسل الهرمي للتصميم.
- استخدِم
MovableContentلنقل العناصر القابلة للإنشاء التي تحتفظ بحالتها بسلاسة. يمكن لنسخMovableContentنقل القيم المحفوظة والمحتفظ بها من مواقعها القديمة إلى المواقع الجديدة.
تذكُّر الدوال الأصلية
على الرغم من أنّ واجهات مستخدم Compose تتكوّن من دوال قابلة للإنشاء، إلا أنّ العديد من العناصر تُستخدم في إنشاء تركيبة وتنظيمها. وأحد الأمثلة الشائعة على ذلك هو الكائنات المعقّدة القابلة للإنشاء التي تحدّد حالتها الخاصة، مثل LazyList، والتي تقبل LazyListState.
عند تحديد العناصر التي تركّز على Compose، ننصحك بإنشاء دالة remember
لتحديد السلوك المطلوب للتذكُّر، بما في ذلك كل من مدة البقاء
والمدخلات الرئيسية. يتيح ذلك لمستهلكي حالتك إنشاء مثيلات بثقة في التسلسل الهرمي للتكوين، وستبقى هذه المثيلات صالحة وسيتم إبطالها على النحو المتوقّع. عند تحديد دالة مصنع قابلة للإنشاء، اتّبِع الإرشادات التالية:
- أضِف البادئة
rememberإلى اسم الدالة. يمكنك بدلاً من ذلك استخدام البادئةretainإذا كان تنفيذ الدالة يعتمد على الكائنretainedولن تتطوّر واجهة برمجة التطبيقات أبدًا لتعتمد على صيغة مختلفة منremember. - استخدِم
rememberSaveableأوrememberSerializableإذا تم اختيار الاحتفاظ بالحالة وكان من الممكن كتابة تنفيذSaverصحيح. - تجنَّب الآثار الجانبية أو القيم الأولية المستندة إلى
CompositionLocalالتي قد لا تكون ذات صلة بالاستخدام. تذكَّر أنّ المكان الذي يتم فيه إنشاء حالتك قد لا يكون هو المكان الذي يتم فيه استهلاكها.
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }