حفظ حالة واجهة المستخدم في Compose

اعتمادًا على الموضع الذي يتم فيه نقل الحالة والمنطق المطلوب، يمكنك استخدام واجهات برمجة تطبيقات مختلفة لتخزين حالة واجهة المستخدم واستعادتها. يستخدم كل تطبيق مزيجًا من واجهات برمجة التطبيقات لتحقيق ذلك على أفضل وجه.

يمكن أن يفقد أي تطبيق Android حالة واجهة المستخدم بسبب إعادة إنشاء النشاط أو العملية. يمكن أن يحدث فقدان الحالة بسبب الأحداث التالية:

يُعدّ الحفاظ على الحالة بعد هذه الأحداث أمرًا ضروريًا لتوفير تجربة إيجابية للمستخدم. يعتمد اختيار الحالة التي يجب الاحتفاظ بها على مسارات المستخدم الفريدة في تطبيقك. في إطار أفضل الممارسات، يجب الاحتفاظ على الأقل ببيانات أدخلها المستخدم والحالة المرتبطة بالتنقّل. وتشمل أمثلة ذلك موضع التمرير في قائمة أو معرّف العنصر الذي يريد المستخدم الحصول على مزيد من التفاصيل عنه أو الاختيار قيد التقدّم للإعدادات المفضّلة للمستخدم أو الإدخال في حقول النص.

تلخّص هذه الصفحة واجهات برمجة التطبيقات المتاحة لتخزين حالة واجهة المستخدم استنادًا إلى الموضع الذي يتم فيه نقل الحالة والمنطق الذي يحتاج إليها.

منطق واجهة المستخدم

إذا تم نقل الحالة في واجهة المستخدم، سواء في الدوال القابلة للإنشاء أو فئات حامل الحالة العادية المحدّدة النطاق في التركيبة، يمكنك استخدام rememberSaveable للاحتفاظ بالحالة أثناء إعادة إنشاء النشاط والعملية.

في المقتطف التالي، يتم استخدام rememberSaveable لتخزين حالة عنصر في واجهة المستخدم منطقي واحد:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

الشكل 1. تتوسّع فقاعة رسالة المحادثة وتتقلّص عند النقر عليها.

showDetails هو متغيّر منطقي يخزّن ما إذا كانت فقاعة المحادثة مصغّرة أو موسّعة.

rememberSaveable يخزّن حالة عنصر في واجهة المستخدم في Bundle من خلال آلية حالة المثيل المحفوظ.

يمكنه تخزين الأنواع الأساسية في الحزمة تلقائيًا. إذا كانت الحالة محفوظة في نوع غير أساسي، مثل فئة بيانات، يمكنك استخدام آليات تخزين مختلفة، مثل استخدام التعليق التوضيحي Parcelize أو استخدام واجهات برمجة تطبيقات Compose مثل listSaver وmapSaver أو تنفيذ فئة حافظة مخصّصة توسّع فئة Saver في وقت تشغيل Compose. اطّلِع على مستندات طرق تخزين الحالة لمعرفة المزيد عن هذه الطرق.

في المقتطف التالي، تخزّن واجهة برمجة التطبيقات rememberLazyListState Compose السمة LazyListState، التي تتألف من حالة التمرير في LazyColumn أو LazyRow، باستخدام rememberSaveable. تستخدم هذه الواجهة LazyListState.Saver، وهي حافظة مخصّصة يمكنها تخزين حالة التمرير واستعادتها. بعد إعادة إنشاء نشاط أو عملية (على سبيل المثال، بعد تغيير الإعدادات مثل تغيير اتجاه الجهاز)، يتم الاحتفاظ بحالة التمرير.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

أفضل الممارسات

rememberSaveable يستخدم Bundle لتخزين حالة واجهة المستخدم، والتي تشاركها واجهات برمجة تطبيقات أخرى تكتب إليها أيضًا، مثل onSaveInstanceState() استدعاءات في نشاطك. ومع ذلك، يكون حجم هذه السمة Bundle محدودًا، وقد يؤدي تخزين كائنات كبيرة إلى ظهور استثناءات TransactionTooLarge في وقت التشغيل. يمكن أن يكون ذلك مشكلة بشكل خاص في تطبيقات Activity الفردية حيث يتم استخدام السمة Bundle نفسها في جميع أنحاء التطبيق.

لتجنُّب هذا النوع من الأعطال، يجب عدم تخزين كائنات كبيرة أو معقّدة أو قوائم من الكائنات في الحزمة.

بدلاً من ذلك، خزِّن الحد الأدنى من الحالة المطلوبة، مثل المعرّفات أو المفاتيح، واستخدِمها لتفويض استعادة حالة واجهة المستخدم الأكثر تعقيدًا إلى آليات أخرى، مثل وحدة التخزين الثابتة.

تعتمد خيارات التصميم هذه على حالات الاستخدام المحدّدة لتطبيقك وكيفية توقّع المستخدمين لسلوكه.

التحقّق من استعادة الحالة

يمكنك التحقّق من استعادة الحالة المخزّنة باستخدام rememberSaveable في عناصر Compose بشكل صحيح عند إعادة إنشاء النشاط أو العملية. تتوفّر واجهات برمجة تطبيقات محدّدة لتحقيق ذلك، مثل StateRestorationTester. اطّلِع على مست0}الاختبار لمعرفة المزيد.

منطق المؤسسة

إذا تم نقل حالة عنصر في واجهة المستخدم UI element state إلى ViewModel لأنّ منطق المؤسسة يتطلب ذلك ، يمكنك استخدام واجهات برمجة تطبيقات ViewModel.

من المزايا الرئيسية لاستخدام ViewModel في تطبيق Android أنّه يعالج تغييرات الإعدادات مجانًا. عند حدوث تغيير في الإعدادات وإيقاف النشاط وإعادة إنشائه، يتم الاحتفاظ في الذاكرة بحالة واجهة المستخدم التي تم نقلها إلى ViewModel. بعد إعادة الإنشاء، يتم إرفاق مثيل ViewModel القديم بمثيل النشاط الجديد.

ومع ذلك، لا يبقى مثيل ViewModel بعد إيقاف العملية نهائيًا من قِبل النظام. للحفاظ على حالة واجهة المستخدم بعد هذا الإجراء، استخدِم وحدة "الحالة المحفوظة" في `ViewModel`، التي تحتوي على واجهة برمجة التطبيقات SavedStateHandle.

أفضل الممارسات

SavedStateHandle تستخدم أيضًا آلية Bundle لتخزين حالة واجهة المستخدم، لذا يجب استخدامها فقط لتخزين حالة عنصر في واجهة المستخدم بسيط.

حالة واجهة مستخدم الشاشة، التي يتم إنتاجها من خلال تطبيق قواعد المؤسسة والوصول إلى طبقات تطبيقك بخلاف واجهة المستخدم، يجب عدم تخزينها في SavedStateHandle بسبب تعقيدها وحجمها المحتملَين. يمكنك استخدام آليات مختلفة لتخزين البيانات المعقّدة أو الكبيرة، مثل وحدة التخزين الثابتة المحلية storage. بعد إعادة إنشاء العملية، تتم إعادة إنشاء الشاشة باستخدام الحالة المؤقتة المستعادة التي تم تخزينها في SavedStateHandle (إن وُجدت)، ويتم إنتاج حالة واجهة مستخدم الشاشة مرة أخرى من طبقة البيانات.

واجهات برمجة تطبيقات SavedStateHandle

SavedStateHandle تتضمّن واجهات برمجة تطبيقات مختلفة لتخزين حالة عنصر في واجهة المستخدم، وأبرزها:

‫Compose State saveable()
StateFlow getStateFlow()

‫Compose State

استخدِم واجهة برمجة التطبيقات saveable في SavedStateHandle لقراءة حالة عنصر في واجهة المستخدم وكتابتها كـ MutableState، ما يضمن بقاءها بعد إعادة إنشاء النشاط والعملية بأقل إعداد للرمز.

تتوافق واجهة برمجة التطبيقات saveable مع الأنواع الأساسية خارج نطاقها، وتتلقّى مَعلمة stateSaver لاستخدام الحافظات المخصّصة، تمامًا مثل rememberSaveable().

في المقتطف التالي، يخزّن message بيانات أدخلها المستخدم التي تم إدخالها في TextField:

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

اطّلِع على مستندات SavedStateHandle لمزيد من المعلومات حول استخدام واجهة برمجة التطبيقات saveable.

StateFlow

استخدِم getStateFlow() لتخزين حالة عنصر في واجهة المستخدم واستهلاكها كتدفق من الـ SavedStateHandle. StateFlow للقراءة فقط، وتتطلب واجهة برمجة التطبيقات تحديد مفتاح حتى تتمكّن من استبدال التدفق لإصدار قيمة جديدة. باستخدام المفتاح الذي ضبطته، يمكنك استرداد StateFlow وجمع أحدث قيمة.

في المقتطف التالي، savedFilterType هو متغيّر StateFlow يخزّن نوع فلتر يتم تطبيقه على قائمة قنوات محادثة في تطبيق محادثة:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

في كل مرة يختار فيها المستخدم نوع فلتر جديدًا، يتم استدعاء setFiltering. يؤدي ذلك إلى حفظ قيمة جديدة في SavedStateHandle مخزّنة باستخدام المفتاح _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType هو تدفق يصدر أحدث قيمة مخزّنة في المفتاح. تم الاشتراك في filteredChannels في التدفق لإجراء فلترة القناة.

اطّلِع على مستندات SavedStateHandle لمزيد من المعلومات حول واجهة برمجة التطبيقات getStateFlow().

ملخّص

يلخّص الجدول التالي واجهات برمجة التطبيقات التي تم تناولها في هذا القسم، ومتى يجب استخدام كل منها لحفظ حالة واجهة المستخدم:

الحدث منطق واجهة المستخدم منطق المؤسسة في ViewModel
تغييرات في الإعدادات rememberSaveable تلقائي
إيقاف العملية نهائيًا من قِبل النظام rememberSaveable SavedStateHandle

تعتمد واجهة برمجة التطبيقات التي يجب استخدامها على الموضع الذي يتم فيه الاحتفاظ بالحالة والمنطق الذي تتطلبه. بالنسبة إلى الحالة المستخدَمة في منطق واجهة المستخدم، استخدِم rememberSaveable. بالنسبة إلى الحالة المستخدَمة في منطق المؤسسة، إذا كنت تحتفظ بها في ViewModel، احفظها باستخدام SavedStateHandle.

يجب استخدام واجهات برمجة تطبيقات الحزمة (rememberSaveable وSavedStateHandle) لتخزين كميات صغيرة من حالة واجهة المستخدم. هذه البيانات هي الحد الأدنى اللازم لاستعادة واجهة المستخدم إلى حالتها السابقة، بالإضافة إلى آليات التخزين الأخرى. على سبيل المثال، إذا خزّنت معرّف ملف شخصي كان المستخدم يطّلِع عليه في الحزمة، يمكنك جلب بيانات كبيرة، مثل تفاصيل الملف الشخصي، من طبقة البيانات.

لمزيد من المعلومات حول الطرق المختلفة لحفظ حالة واجهة المستخدم، اطّلِع على مستندات حفظ حالة واجهة المستخدم العامة وصفحة طبقة البيانات في دليل البنية.