طول عمرها را در Compose بیان کنید

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

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

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

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

remember

retain

rememberSaveable ، rememberSerializable

ارزش ها از بازترکیب جان سالم به در می برند؟

ارزش‌ها از فعالیت‌ها و تفریحات جان سالم به در می‌برند؟

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

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

آیا ارزش‌ها در فرآیند مرگ زنده می‌مانند؟

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

همه

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

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

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

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

remember

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

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

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

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

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

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

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

rememberSaveable و rememberSerializable

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

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

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

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

  • مقادیری که شما به خاطر می‌سپارید باید توسط یک یا چند نوع داده زیر قابل نمایش باشند: مقادیر اولیه (شامل Int ، Long ، Float ، DoubleString یا آرایه‌هایی از هر یک از این نوع‌ها.
  • وقتی یک مقدار ذخیره شده بازیابی می‌شود، نمونه جدیدی خواهد بود که برابر با ( == ) است، اما همان مرجع ( === ) که ترکیب قبلاً از آن استفاده می‌کرد، نیست.

برای ذخیره انواع داده‌های پیچیده‌تر بدون استفاده از 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

API retain از نظر مدت زمانی که مقادیر خود را به خاطر می‌سپارد، بین remember و rememberSaveable / rememberSerializable قرار دارد. این API به این دلیل متفاوت نامگذاری شده است که مقادیر ذخیره شده، چرخه عمر متفاوتی نسبت به مقادیر ذخیره شده مشابه خود دارند.

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

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

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

برای کاربران حرفه‌ای که الگوهای معماری برنامه سفارشی را خارج از توصیه‌های معماری برنامه مدرن اندروید طراحی می‌کنند: retain می‌توان برای ساخت یک API داخلی "شبیه ViewModel " نیز استفاده کرد. اگرچه پشتیبانی از کوروتین‌ها و saved-state به صورت آماده ارائه نمی‌شود، retain می‌تواند به عنوان بلوک سازنده برای چرخه عمر چنین ViewModel -likeهایی با این ویژگی‌های ساخته شده در بالا عمل کند. جزئیات نحوه طراحی چنین کامپوننتی خارج از محدوده این راهنما است.

retain

ViewModel

محدوده‌بندی

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

ViewModel ها واحدهای تکی درون ViewModelStore هستند.

تخریب

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

وقتی ViewModelStore پاک یا از بین می‌رود

قابلیت‌های اضافی

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

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

متعلق به

RetainedValuesStore

ViewModelStore

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

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

ترکیب retain و rememberSaveable یا rememberSerializable

گاهی اوقات، یک شیء نیاز دارد که طول عمر ترکیبی از retained و rememberSaveable یا rememberSerializable داشته باشد. این می‌تواند نشانه‌ای باشد که شیء شما باید یک ViewModel باشد که می‌تواند از وضعیت ذخیره شده پشتیبانی کند، همانطور که در راهنمای ماژول Saved State برای 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 یا شبیه‌سازی بازسازی Activity فراهم می‌کند.

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

قابلیت ذخیره موقعیت مکانی و طرح‌بندی‌های تطبیقی

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

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

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

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

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

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

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

  • نام تابع را با remember شروع کنید. در صورت تمایل، اگر پیاده‌سازی تابع به شیء retained بستگی دارد و API هرگز به گونه‌ای تکامل نمی‌یابد که به نوع دیگری از remember باشد، از پیشوند retain استفاده کنید.
  • اگر ماندگاری حالت انتخاب شده باشد و نوشتن یک پیاده‌سازی صحیح Saver امکان‌پذیر باشد، rememberSaveable یا rememberSerializable استفاده کنید.
  • از عوارض جانبی یا مقداردهی اولیه مقادیر بر اساس CompositionLocal که ممکن است به کاربرد مربوط نباشند، خودداری کنید. به یاد داشته باشید، مکانی که state شما ایجاد می‌شود ممکن است جایی نباشد که مصرف می‌شود.

@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) }
    )
}