ذخیره وضعیت واسط کاربر در Compose

بسته به جایی که وضعیت شما به آن منتقل می‌شود و منطقی که لازم است، می‌توانید از میاناهای برنامه‌سازی کاربردی مختلفی برای ذخیره و بازیابی وضعیت میانای کاربر خود استفاده کنید. هر برنامه از ترکیبی از API-ها برای دستیابی به بهترین نتیجه استفاده می‌کند.

هر برنامه Android ممکن است به‌دلیل بازسازی فعالیت یا فرایند، وضعیت UI خود را ازدست بدهد. این ازدست رفتن حالت می‌تواند به‌دلیل رویدادهای زیر رخ دهد:

حفظ وضعیت پس‌از این رویدادها برای تجربه کاربری مثبت ضروری است. انتخاب اینکه کدام وضعیت حفظ شود به جریان‌های کاربر منحصربه‌فرد برنامه‌تان بستگی دارد. به‌عنوان روال مطلوب، باید حداقل ورودی کاربر و وضعیت مربوط به پیمایش را حفظ کنید. نمونه‌هایی از این شامل موقعیت پیمایش یک فهرست، شناسه موردی که کاربر جزئیات بیشتری درباره آن می‌خواهد، انتخاب درحال انجام اولویت‌های کاربر، یا ورودی در فیلدهای نوشتاری است.

این صفحه APIهای موجود برای ذخیره وضعیت UI را بسته به جایی که وضعیت شما به آن منتقل می‌شود و منطقی که به آن نیاز دارد، خلاصه می‌کند.

منطق واسط کاربر

اگر حالت شما در میانای کاربر، چه در تابع‌های ترکیب‌پذیر یا کلاس‌های نگه‌دارنده حالت ساده با دامنه Composition، بالا برده شده است، می‌توانید از 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)
    }
}

شکل ۱. حبابک پیام گپ با تک‌ضرب زدن باز و بسته می‌شود.

showDetails متغیر بولی است که وضعیت جمع یا ازهم باز بودن حبابک گپ را ذخیره می‌کند.

rememberSaveable وضعیت عنصر میانای کاربر را در Bundle ازطریق سازوکار وضعیت نمونه ذخیره‌شده ذخیره می‌کند.

می‌تواند انواع اولیه را به‌طور خودکار در بسته ذخیره کند. اگر حالت شما در نوعی نگهداری می‌شود که اولیه نیست، مانند کلاس داده، می‌توانید از سازوکارهای ذخیره‌سازی مختلفی استفاده کنید، مانند استفاده از حاشیه‌نویسی Parcelize، استفاده از Compose APIs مانند listSaver و mapSaver، یا پیاده‌سازی یک کلاس ذخیره‌کننده سفارشی که کلاس زمان اجرای Compose Saver را گسترش می‌دهد. برای کسب اطلاعات بیشتر درباره این روش‌ها، به سند روش‌های ذخیره کردن وضعیت مراجعه کنید.

In the following snippet, the rememberLazyListState Compose API stores LazyListState, which consists of the scroll state of a LazyColumn or LazyRow, using 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. برای کسب اطلاعات بیشتر، سند آزمایش را بررسی کنید.

منطق کسب‌وکار

اگر وضعیت عنصر میانای کاربر شما به ViewModel ارتقا یافته است چون منطق کسب‌وکار به آن نیاز دارد، می‌توانید از میاناهای برنامه‌سازی کاربردی ViewModel استفاده کنید.

یکی از مزایای اصلی استفاده از ViewModel در برنامه Android شما این است که تغییرات پیکربندی را به‌طور رایگان مدیریت می‌کند. وقتی تغییری در پیکربندی ایجاد می‌شود و فعالیت ازبین می‌رود و دوباره ایجاد می‌شود، حالت واسط کاربر که به ViewModel بالابرده شده است در حافظه نگه داشته می‌شود. پس‌از بازآفرینی، نمونه قدیمی ViewModel به نمونه فعالیت جدید پیوست می‌شود.

بااین‌حال، نمونه ViewModel درصورت مرگ پردازش به‌دست سیستم باقی نمی‌ماند. برای اینکه وضعیت واسط کاربر از این شرایط جان سالم به‌در ببرد، از ماژول Saved State برای ViewModel استفاده کنید که حاوی میانای برنامه‌سازی کاربردی SavedStateHandle است.

روال مطلوب

SavedStateHandle همچنین از سازوکار Bundle برای ذخیره کردن حالت میانای کاربر استفاده می‌کند، بنابراین باید فقط برای ذخیره کردن حالت عنصر میانای کاربر ساده از آن استفاده کنید.

وضعیت میانای کاربری صفحه‌نمایش که با اعمال قوانین کسب‌وکار و دسترسی به لایه‌های برنامه شما به‌غیراز میانای کاربری تولید می‌شود، نباید به‌دلیل پیچیدگی و اندازه احتمالی آن در SavedStateHandle ذخیره شود. می‌توانید از سازوکارهای مختلفی برای ذخیره داده‌های پیچیده یا بزرگ استفاده کنید، مثل فضای ذخیره‌سازی پایدار محلی. پس‌از بازآفرینی فرایند، صفحه با وضعیت گذرا بازیابی‌شده که در SavedStateHandle ذخیره شده بود (درصورت وجود) بازآفرینی می‌شود و وضعیت میانای کاربر صفحه دوباره از لایه داده تولید می‌شود.

SavedStateHandle APIs

SavedStateHandle میاناهای برنامه‌سازی کاربردی مختلفی برای ذخیره کردن وضعیت عنصر میانای کاربر دارد، به‌ویژه:

نوشتن State saveable()
StateFlow getStateFlow()

نوشتن State

از saveable API مربوط به SavedStateHandle برای خواندن و نوشتن وضعیت عنصر میانای کاربر به‌عنوان MutableState استفاده کنید تا با حداقل راه‌اندازی کد، از بازسازی فعالیت و فرایند جان سالم به‌در ببرد.

‫saveable API از انواع اولیه به‌طور پیش‌فرض پشتیبانی می‌کند و پارامتر 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) }
    )
}

برای کسب اطلاعات بیشتر درباره استفاده از میانای برنامه‌سازی کاربردی saveable، به اسناد SavedStateHandle مراجعه کنید.

StateFlow

از getStateFlow() برای ذخیره کردن حالت عنصر میانای کاربر و مصرف آن به‌عنوان جریان از SavedStateHandle استفاده کنید. StateFlow فقط خواندنی است، و API از شما می‌خواهد کلیدی را مشخص کنید تا بتوانید جریان را برای انتشار مقدار جدید جایگزین کنید. با کلیدی که پیکربندی کرده‌اید، می‌توانید 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 در جریان مشترک شده است تا فیلتر کردن کانال را انجام دهد.

برای کسب اطلاعات بیشتر درباره میانای برنامه‌سازی کاربردی getStateFlow()، به مستندات SavedStateHandle مراجعه کنید.

خلاصه

جدول زیر میاناهای برنامه‌سازی کاربردی پوشش‌داده‌شده در این بخش و زمان استفاده از هرکدام برای ذخیره کردن وضعیت میانای کاربر را خلاصه می‌کند:

رویداد منطق واسط کاربر منطق کسب‌وکار در ViewModel
تغییرات پیکربندی rememberSaveable خودکار
مرگ پردازش آغازشده توسط سیستم rememberSaveable SavedStateHandle

میانای برنامه‌سازی کاربردی مورداستفاده به جایی که وضعیت در آن نگهداری می‌شود و منطقی که نیاز دارد بستگی دارد. برای وضعیتی که در منطق میانای کاربر استفاده می‌شود، از rememberSaveable استفاده کنید. برای حالتی که در منطق کسب‌وکار استفاده می‌شود، اگر آن را در ViewModel نگه می‌دارید، آن را بااستفاده از SavedStateHandle ذخیره کنید.

باید از میاناهای برنامه‌سازی کاربردی بسته (rememberSaveable و SavedStateHandle) برای ذخیره کردن مقادیر کوچک وضعیت میانای کاربر استفاده کنید. این داده‌ها حداقل داده‌های لازم برای بازیابی واسط کاربر به حالت قبلی‌اش است، به‌همراه دیگر سازوکارهای ذخیره‌سازی. برای مثال، اگر شناسه نمایه‌ای را که کاربر درحال مشاهده آن بوده است در بسته ذخیره کنید، می‌توانید داده‌های سنگین، مانند جزئیات نمایه، را از لایه داده واکشی کنید.

برای اطلاعات بیشتر درباره روش‌های مختلف ذخیره کردن وضعیت واسط کاربر، به سند ذخیره کردن وضعیت واسط کاربر و صفحه لایه داده در راهنمای معماری مراجعه کنید.