في Jetpack Compose، غالبًا ما تحتفظ الدوال المركّبة بالحالة باستخدام الدالة remember. يمكن إعادة استخدام القيم التي يتم تذكّرها في عمليات إعادة الإنشاء، كما هو موضّح في State وJetpack Compose.
While remember serves as a tool to persist values across recompositions, state
often needs to live beyond the lifetime of a composition. تشرح هذه الصفحة الفرق بين واجهات برمجة التطبيقات 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 مخصّص لتسلسل العنصر وإلغاء تسلسله إلى
أنواع بيانات متوافقة. يُرجى العِلم أنّ ميزة "الكتابة" تفهم أنواع البيانات الشائعة، مثل
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).
في مقابل دورة الحياة الأقصر من rememberSaveable، يمكن لـ retain
الاحتفاظ بالقيم التي لا يمكن تسلسلها، مثل تعبيرات lambda والتدفقات
والكائنات الكبيرة مثل الصور النقطية. على سبيل المثال، يمكنك استخدام 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
في الوقت نفسه. ويؤدي الجمع بينهما بشكل صحيح إلى زيادة التعقيد بشكل كبير.
ننصحك باستخدام هذا النمط كجزء من أنماط البنية الأكثر تقدّمًا وتخصيصًا، وفقط عند استيفاء جميع الشروط التالية:
- إذا كنت تحدّد كائنًا يتألف من مزيج من القيم التي يجب الاحتفاظ بها أو حفظها (مثل كائن يتتبّع إدخال المستخدم وذاكرة تخزين مؤقت في الذاكرة لا يمكن كتابتها على القرص)
- يتم تحديد نطاق الحالة لدالة مركّبة، وهي غير مناسبة لنطاق singleton
أو مدة صلاحية
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) } ) }