ذخیره وضعیت واسط کاربر در 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)
    }
}

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

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

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

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

در گزیده زیر، rememberLazyListState API «نوشتن» 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 در عناصر «نوشتن» شما وقتی فعالیت یا فرایند بازآفرینی می‌شود به‌درستی بازیابی شود. میاناهای برنامه‌سازی کاربردی خاصی برای دستیابی به این هدف وجود دارد، مثلاً StateRestorationTester. برای کسب اطلاعات بیشتر، مستندات آزمایش را بررسی کنید.

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

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

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

بااین‌حال، نمونه ViewModel درصورت بسته شدن پردازش توسط سیستم، ازدست می‌رود. برای اینکه وضعیت واسط کاربر از این وضعیت جان سالم به‌در ببرد، از واحد «وضعیت ذخیره‌شده» برای ViewModel استفاده کنید که حاوی SavedStateHandle API است.

روال مطلوب

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

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

‫SavedStateHandle میانای برنامه‌سازی کاربردی

‫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 API، به اسناد 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() API، مستندات SavedStateHandle را ببینید.

خلاصه

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

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

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

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

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