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

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

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

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

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

remember

retain

rememberSaveable، rememberSerializable

مقادیر در ترکیب‌های مجدد باقی می‌مانند؟

✅

✅

✅

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

❌

✅

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

✅

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

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

❌

❌

✅

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

همه

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

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

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

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

remember

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

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

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

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

  • ایجاد کردن اشیای وضعیت داخلی، مثل وضعیت پیمایش یا وضعیت پویانمایی
  • اجتناب از بازآفرینی پرهزینه شیء در هر ترکیب مجدد

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

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

‫rememberSaveable و rememberSerializable

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

‫rememberSerializable عملکردی مشابه rememberSaveable دارد، اما به‌طور خودکار از انواع پیچیده‌ای که با کتابخانه kotlinx.serialization قابل‌سریال‌سازی هستند پشتیبانی می‌کند. اگر نوع شما با @Serializable علامت‌گذاری شده است (یا می‌تواند علامت‌گذاری شود)، rememberSerializable را انتخاب کنید و در همه موارد دیگر، 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

‫API retain ازنظر مدت زمانی که مقادیرش را در حافظه ذخیره می‌کند بین 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 مدیریت می‌شوند و طول عمر متفاوتی از مقادیر نگهداری‌شده دارند. اگرچه ViewModel تا زمانی که ViewModelStore آن ازبین نرفته است فعال باقی می‌ماند، مقادیر نگهداری‌شده زمانی که محتوا به‌طور دائمی از ترکیب حذف می‌شود، بازنشسته می‌شوند (برای تغییر پیکربندی، به‌عنوان مثال، این به این معنی است که اگر سلسله‌مراتب رابط کاربری بازسازی شود و مقدار نگهداری‌شده پس‌از بازسازی ترکیب مصرف نشود، مقدار نگهداری‌شده بازنشسته می‌شود).

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

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

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

retain

ViewModel

Scoping

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

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

تخریب

هنگام خروج دائمی از سلسله‌مراتب ترکیب

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

عملکرد اضافی

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

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

مالک

RetainedValuesStore

ViewModelStore

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

  • مقادیر مختص واسط کاربر که در نمونه‌های جداگانه عنصر ترکیبی محلی‌سازی شده است
  • پیگیری ظهور، احتمالاً ازطریق RetainedEffect
  • واحد سازنده برای تعریف معماری سفارشی «شبیه ViewModel» عنصر
  • استخراج تعاملات بین لایه‌های داده و میانای کاربر در یک کلاس جداگانه، هم برای سازمان‌دهی کد و هم برای آزمایش
  • تبدیل Flow به State شیء و فراخوانی توابع تعلیق که نباید با تغییرات پیکربندی مختل شوند
  • هم‌رسانی وضعیت‌ها در بخش‌های بزرگ واسط کاربر، مثل کل صفحه‌نمایش
  • هم‌کنش‌پذیری با View

ترکیب retain و rememberSaveable یا rememberSerializable

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

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

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

هنگام تعریف کردن اشیای متمرکز بر «نوشتن»، توصیه می‌کنیم remember تابعی برای تعریف رفتار به‌یادآوری موردنظر ایجاد کنید، که شامل هر دو ورودی کلیدی و طول عمر می‌شود. این کار به مصرف‌کنندگان وضعیت شما امکان می‌دهد با اطمینان نمونه‌هایی در سلسله‌مراتب ترکیب ایجاد کنند که باقی می‌مانند و همان‌طور که انتظار می‌رود نامعتبر می‌شوند. هنگام تعریف تابع کارخانه ترکیب‌پذیر، این دستورالعمل‌ها را دنبال کنید:

  • نام تابع را با remember پیشوندگذاری کنید. درصورت تمایل، اگر پیاده‌سازی تابع به retained بودن شیء بستگی دارد و API هرگز به گونه‌ای تکامل نخواهد یافت که به نوع دیگری از 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) }
    )
}