طول عمر وضعیت در Compose

در Jetpack Compose، توابع ترکیب‌پذیر اغلب بااستفاده از تابع remember حالت را نگه می‌دارند. همان‌طور که در حالت و Jetpack Compose توضیح داده شده است، مقادیری که به‌خاطر سپرده می‌شوند می‌توانند در بازترکیب‌ها دوباره استفاده شوند.

درحالی‌که remember به‌عنوان ابزاری برای حفظ مقادیر در ترکیب‌بندی‌های مجدد عمل می‌کند، حالت اغلب باید فراتر از طول عمر ترکیب‌بندی باقی بماند. این صفحه تفاوت بین میاناهای برنامه‌سازی کاربردی remember،‏ retain،‏ rememberSaveable، و rememberSerializable، زمان انتخاب هر میانای برنامه‌سازی کاربردی، و بهترین روش‌ها برای مدیریت مقادیر به‌خاطرسپرده‌شده و حفظ‌شده در Compose را توضیح می‌دهد.

طول عمر صحیح را انتخاب کنید

در Compose، چندین تابع وجود دارد که می‌توانید برای حفظ وضعیت در ترکیب‌بندی‌ها و فراتر از آن استفاده کنید: remember، retain، rememberSaveable، و rememberSerializable. این توابع ازنظر طول عمر و معناشناسی متفاوت هستند و هرکدام برای ذخیره انواع خاصی از وضعیت مناسب هستند. تفاوت‌ها در جدول زیر به‌طور خلاصه بیان شده است:

remember

retain

rememberSaveable، rememberSerializable

آیا مقادیر در بازترکیب‌ها حفظ می‌شوند؟

✅

✅

✅

آیا مقادیر در بازسازی فعالیت باقی می‌مانند؟

❌

✅

همیشه همان نمونه (===) برگردانده خواهد شد

✅

یک شیء معادل (==) برگردانده خواهد شد، احتمالاً یک کپی غیرسریالی‌شده

آیا مقادیر از مرگ پردازش جان سالم به‌در می‌برند؟

❌

❌

✅

انواع داده‌های پشتیبانی‌شده

همه

نباید به هیچ شیئی که درصورت ازبین رفتن فعالیت لو می‌رود ارجاع دهد

باید قابل‌سریال‌سازی باشد
(یا با Saver سفارشی یا با kotlinx.serialization)

موارد استفاده

  • اشیایی که در محدوده ترکیب‌بندی هستند
  • اشیاء پیکربندی برای composables
  • وضعیتی که می‌تواند بدون ازدست دادن وفاداری واسط کاربر بازسازی شود
  • حافظه‌های پنهان
  • اشیای «مدیر» یا با عمر طولانی
  • درونداد کاربر
  • حالتی که برنامه نمی‌تواند آن را بازآفرینی کند، ازجمله ورودی فیلد نوشتاری، حالت پیمایش، کلیدهای روشن/خاموش، و غیره.

remember

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

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

وقتی از مقدار به‌خاطر سپرده‌شده دیگر استفاده نشود، فراموش می‌شود و سابقه آن دور انداخته می‌شود. مقادیر به‌خاطر سپرده‌شده وقتی از سلسله‌مراتب ترکیب‌بندی برداشته شوند (ازجمله وقتی مقداری برداشته شود و دوباره اضافه شود تا بدون استفاده از ترکیب‌شدنی key یا MovableContent به مکان دیگری منتقل شود) یا با پارامترهای key متفاوت فراخوانی شوند، فراموش می‌شوند.

از بین انتخاب‌های موجود، remember کوتاه‌ترین طول عمر را دارد و مقادیر را زودتر از چهار تابع یادسپاری شرح‌داده‌شده در این صفحه فراموش می‌کند. این ویژگی آن را برای موارد زیر مناسب‌تر می‌کند:

  • ایجاد اشیاء حالت داخلی، مانند موقعیت پیمایش یا حالت پویانمایی
  • اجتناب از بازآفرینی شیء گران‌قیمت در هر بازترکیب

بااین‌حال، باید از موارد زیر اجتناب کنید:

  • ذخیره کردن هرگونه ورودی کاربر با remember، زیرا اشیای به‌خاطر سپرده‌شده در تغییرات پیکربندی Activity و مرگ پردازش آغازشده توسط سیستم فراموش می‌شوند.

‫rememberSaveable و rememberSerializable

‫rememberSaveable و rememberSerializable بر پایه remember ساخته شده‌اند. این توابع طولانی‌ترین طول عمر را در میان توابع یادسپاری موردبحث در این راهنما دارند. علاوه‌بر به‌خاطر سپردن موقعیتی اشیاء در بازترکیب‌ها، می‌تواند مقادیر را ذخیره کند تا بتوان آن‌ها را در بازآفرینی‌های فعالیت بازیابی کرد، ازجمله از تغییرات پیکربندی و مرگ پردازش (زمانی که سیستم پردازش برنامه شما را درحالی‌که در پس‌زمینه است متوقف می‌کند، معمولاً برای آزاد کردن حافظه برای برنامه‌های پیش‌زمینه یا اگر کاربر اجازه‌ها را از برنامه شما درحین اجرا لغو کند).

rememberSerializable مانند rememberSaveable کار می‌کند، اما به‌طور خودکار از انواع پیچیده ماندگار که با کتابخانه kotlinx.serialization قابل‌سریال‌سازی هستند پشتیبانی می‌کند. اگر نوع شما با @Serializable علامت‌گذاری شده است (یا می‌تواند علامت‌گذاری شود)، rememberSerializable را انتخاب کنید و در همه موارد دیگر، rememberSaveable را انتخاب کنید.

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

توجه داشته باشید که rememberSaveable و rememberSerializable مقادیر به‌خاطرسپرده‌شده خود را با سریالی کردن آن‌ها در Bundle ذخیره می‌کنند. این موضوع دو پیامد دارد:

  • مقادیری که به‌خاطر می‌سپارید باید با یک یا چند نوع داده زیر قابل‌نمایش باشند: Primitives (شامل 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 API ازنظر مدت‌زمانی که مقادیرش را یادداشت می‌کند، بین remember و rememberSaveable/rememberSerializable قرار دارد. نام آن متفاوت است زیرا مقادیر حفظ‌شده نیز چرخه حیات متفاوتی را نسبت‌به همتایان به‌خاطر سپرده‌شده خود تجربه می‌کنند.

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

درعوض این چرخه حیات کوتاه‌تر از rememberSaveable، retain می‌تواند مقادیری را که نمی‌توانند سریالی شوند، مانند عبارت‌های لامبدا، جریان‌ها، و اشیاء بزرگ مانند نقشه‌های بیتی، حفظ کند. برای مثال، می‌توانید از 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 مدیریت می‌شوند و طول عمر متفاوتی نسبت به مقادیر حفظ‌شده دارند. While a ViewModel will remain active until its ViewModelStore is destroyed, retained values are retired when the content is permanently removed from the composition (for a configuration change, as an example, this means that a retained value is retired if the UI hierarchy is recreated and the retained value wasn't consumed after the composition is recreated).

ViewModel همچنین شامل ادغام‌های آماده برای تزریق وابستگی با Dagger و Hilt، ادغام با SavedState، و پشتیبانی از کوروتین داخلی برای راه‌اندازی کارهای پس‌زمینه است. این امر ViewModel را به مکانی ایده‌آل برای راه‌اندازی کارهای پس‌زمینه و درخواست‌های شبکه، تعامل با دیگر منابع داده در پروژه‌تان، و به‌طور اختیاری ضبط و حفظ وضعیت واسط کاربر حیاتی برای مأموریت تبدیل می‌کند که باید هم در تغییرات پیکربندی در ViewModel حفظ شود و هم از مرگ پردازش جان سالم به‌در ببرد.

‫retain برای اشیایی که به نمونه‌های ترکیب‌پذیر خاصی محدود شده‌اند و نیازی به استفاده مجدد یا هم‌رسانی بین ترکیب‌پذیرهای هم‌رده ندارند، مناسب‌ترین گزینه است. درحالی‌که ViewModel به‌عنوان مکان خوبی برای ذخیره کردن حالت واسط کاربر و انجام دادن کارهای پس‌زمینه عمل می‌کند، retain گزینه خوبی برای ذخیره کردن اشیاء برای لوله‌کشی واسط کاربر مانند حافظه‌های پنهان، ردیابی و تجزیه‌وتحلیل تأثیر، وابستگی به AndroidView، و دیگر اشیاء است که با سیستم‌عامل Android تعامل دارند یا کتابخانه‌های طرف سوم مانند پردازشگرهای پرداخت یا تبلیغات را مدیریت می‌کنند.

برای کاربران پیشرفته‌ای که الگوهای معماری برنامه سفارشی را خارج از توصیه‌های معماری برنامه Modern Android طراحی می‌کنند: retain همچنین می‌تواند برای ساختن میانای برنامه‌سازی کاربردی داخلی «شبیه به ViewModel» استفاده شود. اگرچه پشتیبانی از روال‌های هم‌زمان و وضعیت ذخیره‌شده به‌طور پیش‌فرض ارائه نمی‌شود، retain می‌تواند به‌عنوان بلوک ساختمانی برای چرخه حیات چنین ViewModel-شبیه‌سازی‌هایی با این ویژگی‌ها در بالا ساخته شود. جزئیات نحوه طراحی چنین مؤلفه‌ای خارج از محدوده این راهنما است.

retain

ViewModel

محدوده

مقادیر مشترک وجود ندارد؛ هر مقدار در نقطه خاصی در سلسله‌مراتب ترکیب‌بندی حفظ می‌شود و با آن مرتبط است. حفظ همان نوع در مکان متفاوت همیشه روی نمونه جدیدی عمل می‌کند.

ViewModelها در ViewModelStore تک‌عضو هستند

تخریب

هنگام ترک دائمی سلسله‌مراتب ترکیب‌بندی

وقتی ViewModelStore پاک یا نابود شود

عملکرد اضافی

می‌تواند زمانی که شیء در سلسله‌مراتب ترکیب‌بندی است یا نه، فراخوانی‌های برگشتی را دریافت کند

‫coroutineScope داخلی، پشتیبانی از SavedStateHandle، می‌تواند بااستفاده از Hilt تزریق شود

متعلق به

RetainedValuesStore

ViewModelStore

موارد استفاده

  • Persisting UI-specific values local to individual composable instances
  • ردگیری ظهور، احتمالاً ازطریق RetainedEffect
  • واحد سازنده برای تعریف مؤلفه معماری سفارشی «شبیه ViewModel»
  • استخراج تعاملات بین میانای کاربر و لایه‌های داده در یک کلاس جداگانه، هم برای سازمان‌دهی کد و هم برای آزمایش کردن
  • تبدیل Flow به اشیاء State و فراخوانی عملکردهای تعلیق که نباید با تغییرات پیکربندی قطع شوند
  • هم‌رسانی وضعیت‌ها در نواحی بزرگ واسط کاربر، مثل کل صفحه‌نمایش
  • هم‌کنش‌پذیری با View

ترکیب retain و rememberSaveable یا rememberSerializable

گاهی اوقات، یک شیء باید طول عمر ترکیبی از هر دو retained و rememberSaveable یا rememberSerializable داشته باشد. این ممکن است نشانگر این باشد که شیء شما باید ViewModel باشد که می‌تواند از وضعیت ذخیره‌شده پشتیبانی کند، همان‌طور که در راهنمای ماژول وضعیت ذخیره‌شده برای ViewModel شرح داده شده است.

می‌توانید از retain و rememberSaveable یا rememberSerializable به‌طور هم‌زمان استفاده کنید. ترکیب صحیح هر دو چرخه حیات پیچیدگی قابل‌توجهی را اضافه می‌کند. توصیه می‌کنیم از این الگو به‌عنوان بخشی از الگوهای معماری پیشرفته‌تر و سفارشی استفاده کنید، و فقط زمانی که همه موارد زیر درست باشد:

  • شیئی را تعریف می‌کنید که از ترکیبی از مقادیر تشکیل شده است که باید حفظ یا ذخیره شوند (مثلاً شیئی که ورودی کاربر را ردیابی می‌کند و حافظه نهان درون‌حافظه‌ای که نمی‌توان آن را روی دیسک نوشت)
  • وضعیت شما به یک composable محدود شده است و برای محدوده یا طول عمر 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 یا شبیه‌سازی بازآفرینی Activity فراهم می‌کند.

برای مشاهده نمونه کامل نحوه پیاده‌سازی این الگو، نمونه کامل (RetainAndSaveSample.kt) را ببینید.

یادسپاری موقعیتی و چیدمان‌های تطبیقی

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

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

برای مؤلفه‌های آماده استفاده مثل ListDetailPaneScaffold و NavDisplay (از Jetpack Navigation 3)، این موضوع مشکلی ایجاد نمی‌کند و وضعیت شما درطول تغییرات چیدمان حفظ خواهد شد. برای مؤلفه‌های سفارشی که با ابعاد دستگاه سازگار می‌شوند، با انجام یکی از کارهای زیر مطمئن شوید که وضعیت تحت‌تأثیر تغییرات چیدمان قرار نمی‌گیرد:

  • مطمئن شوید که ترکیب‌شدنی‌های حالت‌دار همیشه در یک مکان در سلسله‌مراتب ترکیب‌بندی فراخوانی می‌شوند. چیدمان‌های تطبیقی را با تغییر منطق چیدمان پیاده‌سازی کنید به‌جای اینکه اشیاء را در سلسله‌مراتب ترکیب‌بندی جابه‌جا کنید.
  • از MovableContent برای جابه‌جایی مناسب ترکیب‌پذیرهای حالت‌دار استفاده کنید. نمونه‌های MovableContent می‌توانند مقادیر به‌خاطر سپرده‌شده و حفظ‌شده را از مکان‌های قدیمی به مکان‌های جدید منتقل کنند.

به‌خاطر سپردن توابع کارخانه‌ای

اگرچه رابط‌های کاربری Compose از توابع ترکیب‌پذیر تشکیل شده‌اند، اشیاء زیادی در ایجاد و سازمان‌دهی ترکیب‌بندی نقش دارند. رایج‌ترین مثال برای این مورد اشیاء ترکیب‌پذیر پیچیده‌ای هستند که وضعیت خودشان را تعریف می‌کنند، مثل LazyList، که LazyListState را می‌پذیرد.

هنگام تعریف اشیای متمرکز بر Compose، توصیه می‌کنیم تابع remember ایجاد کنید تا رفتار به‌خاطر سپردن موردنظر را تعریف کنید، ازجمله طول عمر و ورودی‌های کلیدی. این کار به مصرف‌کنندگان وضعیت شما امکان می‌دهد تا با اطمینان نمونه‌هایی را در سلسله‌مراتب ترکیب‌بندی ایجاد کنند که طبق انتظار باقی می‌مانند و نامعتبر می‌شوند. ‫When defining a composable factory function, follow these guidelines:

  • پیشوند نام تابع را با remember شروع کنید. اختیاراً، اگر پیاده‌سازی تابع به شیء retained بستگی دارد و میانای برنامه‌سازی کاربردی هرگز برای تکیه بر نوع دیگری از remember تکامل نخواهد یافت، به‌جای آن از پیشوند retain استفاده کنید.
  • اگر پایداری وضعیت انتخاب شده است و امکان نوشتن پیاده‌سازی صحیح Saver وجود دارد، از rememberSaveable یا rememberSerializable استفاده کنید.
  • از عوارض جانبی یا مقداردهی اولیه براساس 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) }
    )
}