Compose یک فریم را در سه فاز با ترتیب دقیق و جریان رو به جلو اجرا میکند:
- ترکیببندی: توابع
@Composableرا برای ساختن و بهروزرسانی درخت میانای کاربر اجرا میکند. - چیدمان: فرزندان را اندازهگیری میکند و سپس آنها را قرار میدهد.
- ترسیم: فرمانهای ترسیم بوم را برای رندر کردن پیکسلها در صفحه صادر میکند.
هرگاه Compose State درطول هر فازی خوانده شود، Compose بهطور خودکار
یک وابستگی را بین آن حالت و فاز مربوطه ثبت میکند.
چه چیزی باعث میشود نوشتن «برعکس» شود؟
نوشتن معکوس زمانی رخ میدهد که یک حالت در فاز بعدی اصلاح شود (یا در یک محدوده پاییندستی—به معنای یک محدوده قابلترکیب که بعداً در همان گذر ترکیببندی اجرا میشود) نسبت به جایی که خوانده شده است، که Compose را مجبور میکند با زمانبندی یک فاز قبلی یا قابلترکیب برای اجرای مجدد، دوباره ترکیببندی کند. نوشتن معکوس یک حلقه بازترکیب بهینهسازینشده است.
پیامدهای نوشتن دادههای قدیمیتر
نوشتنهای معکوس لزوماً چیز بدی نیستند و همیشه باعث خرابی نمیشوند، اما ناکارآمد هستند و میتوانند به چند روش به عملکرد برنامه آسیب برسانند:
- پرداخت قاب اضافی و قابهای رهاشده: نوشتن معکوس Compose را مجبور میکند تا گذرهای ترکیببندی زائد را در قابهای متوالی اجرا کند، که باعث هدر رفتن منابع CPU و GPU و احتمالاً ایجاد لرزش میشود.
- مشکلات صحت قاب اول: اگر مؤلفه شما برای حل ابعاد یا وضعیت نهایی خود نیاز به نوشتن معکوس داشته باشد، قاب اول با دادههای نامعتبر، پیشفرض، یا نامشخص (مانند اندازه صفر یا انحراف نادرست) رندر میشود. این امر باعث میشود که هنگام رندر شدن قاب دوم، پریدن دیداری یا چشمک زدن چیدمان قابلمشاهده باشد.
- حلقههای بازترکیب بینهایت: اگر تغییر وضعیت اندازه چیدمان را تغییر دهد، و اندازه چیدمان بهطور مداوم مقدار جدیدی را به وضعیت برگرداند، ممکن است با خطر ایجاد حلقه قاب بینهایت مواجه شوید که در آن صفحه بهطور مداوم هر قاب را بازترکیب میکند بدون اینکه هرگز پایدار شود.
جریان روبهجلو در فازها
تغییرات وضعیت همیشه باید در فازها بهجلو جریان داشته باشد:
| مرحله خواندن | نوشتن بافت | قابلقبول است؟ | چرا |
|---|---|---|---|
چیدمان (Modifier.offset { }) |
ترکیببندی | بله | ترکیببندی وضعیت را بهروز میکند → چیدمان آن را بعداً در همان قاب بدون ترکیببندی مجدد میخواند. |
مساوی (graphicsLayer { }، drawBehind { }) |
ترکیببندی | بله | ترکیببندی وضعیت را بهروز میکند → Draw آن را در مرحله نهایی میخواند. «ترکیببندی» و «چیدمان» کاملاً نادیده گرفته میشوند. |
| قرعهکشی | چیدمان | بله | چیدمان وضعیت را بهروز میکند و ترسیم آن را میخواند، جریان معتبر. |
| ترکیببندی | «بازخوان رویداد» (onClick، onValueChange) درحال تغییر وضعیت رانندگی. توجه: بازخوانیهای چیدمان بهعنوان رویداد محسوب نمیشوند. |
بله | رویداد حالتی را تغییر میدهد که برای هدایت ترکیببندی استفاده میشود. اگر رویداد خارج از چارچوب اتفاق بیفتد (نه در Composition، Layout، یا Draw)، این مقدار معتبر است. |
| ترکیببندی | روال همکار (LaunchedEffect) |
بله - با احتیاط | بهطور ناهمزمان وضعیت را در پاسخ به چرخه حیات/رویدادها بهروز میکند. نوشتن از جلوهها میتواند معتبر باشد، اما میتواند به لایهبندی حالت ناکارآمد اشاره کند. درصورت امکان باید از آنها اجتناب شود. |
| جایگذاری (در چیدمان) | اندازهگیری (در چیدمان) | بله | در Layout، بهروزرسانی وضعیت و سپس خواندن آن وضعیت در جایگذاری قابلقبول است. |
| اندازهگیری (در چیدمان) | جایگذاری (در چیدمان) | نه - نوشتن برعکس | نوشتن در وضعیت در جایگذاری که سپس در وضعیت خواندن قرار میگیرد، باعث ایجاد حلقه اندازهگیری مجدد میشود. |
| ترکیببندی | چیدمان (onSizeChanged، LayoutModifier) |
No - Backwards write | چیدمان «ترکیببندی» را نامعتبر میکند ← حلقه «ترکیببندی مجدد». |
| ترکیببندی | مساوی (drawWithContent، Canvas) |
نه - نوشتن برعکس | ترسیم، ترکیببندی را باطل میکند → حلقه بازترکیببندی. |
ترکیبهای فاز: عقب درمقابل جلو
در زیر نمونههایی از نوشتن معکوس در Compose و نحوه حل آنها آورده شده است.
برعکس: خواندن در Composition، نوشتن در Layout
- چه اتفاقی میافتد: Composition مقدار
componentHeightرا میخواند تا تعیین کند چه واسط کاربریای باید منتشر شود. بعداً در قاب، فاز «چیدمان» نماها را اندازهگیری یا جایگذاری میکند و مقدار جدیدی را درcomponentHeightمینویسد (برای مثال، بااستفاده ازonSizeChanged،onGloballyPositioned، یاLayoutModifierسفارشی). - نتیجه: اصلاح
componentHeightدر «چیدمان» فاز «ترکیببندی» را که بهتازگی تکمیل شده است باطل میکند. توجه داشته باشید کهonSizeChangedاندازه را پساز تکمیل شدن مرحله اندازهگیری چیدمان گزارش میکند. اگر مقدار وضعیت بهروزشده در گذر بعدی تثبیت شود، بازترکیب ممکن است پساز یک فریم اضافی متوقف شود؛ اما اگر مقدار جدید همچنان اندازه را تغییر دهد، منجر به حلقه فریم بینهایت میشود. علاوهبراین،onGloballyPositionedپساز هم چیدمان و هم جایگذاری اجرا میشود، که باعث میشود نوشتنهای وضعیت درون آن حتی بیشتر مستعد بازترکیب مداوم و حلقههای بازچینی در سراسر قابهای متوالی باشد.
// ❌ BAD: Read in Composition, Written in Layout (onSizeChanged) @Composable fun BadAspectRatioImage(painter: Painter) { var calculatedHeight by remember { mutableStateOf(0.dp) } val density = LocalDensity.current // State read during COMPOSITION: Image( painter = painter, contentDescription = "Dynamic Image", modifier = Modifier .fillMaxWidth() .height(calculatedHeight) .onSizeChanged { size -> // State write during LAYOUT phase! // Triggers backwards write and recomposition pass val aspectRatio = 16f / 9f val widthDp = with(density) { size.width.toDp() } calculatedHeight = widthDp / aspectRatio } ) } // ✅ GOOD: Measure and calculate aspect ratio height in Phase 2 (Layout) without recomposition @Composable fun GoodAspectRatioImage( painter: Painter, aspectRatio: Float = 16f / 9f, modifier: Modifier = Modifier ) { Layout( content = { Image( painter = painter, contentDescription = "Dynamic Image" ) }, modifier = modifier ) { measurables, constraints -> val width = constraints.maxWidth val height = (width / aspectRatio).toInt() // Illustrative, you can use Modifier.aspectRatio() val imageConstraints = constraints.copy( minWidth = width, maxWidth = width, minHeight = height, maxHeight = height ) val placeable = measurables.first().measure(imageConstraints) layout(width, height) { placeable.placeRelative(0, 0) } } }
برعکس: خواندن در «ترکیببندی»، نوشتن در «طراحی»
- چه اتفاقی میافتد: وضعیت در بدنه Composable (فاز «ترکیببندی») خوانده میشود،
اما در
Modifier.drawWithContent،Modifier.drawBehind، یاCanvas(فاز «ترسیم») جهش پیدا میکند. - نتیجه: فاز ترسیم وضعیت را تغییر میدهد ← ترکیببندی نامعتبر میشود ← حلقه بیپایان.
// ❌ BAD: Read in Composition, Written in Draw () @Composable fun BadBackwardsWriteDraw() { var componentHeight by remember { mutableStateOf(0.dp) } // State read during COMPOSITION: Text( text = "Height is: $componentHeight", modifier = Modifier.drawBehind { // State write during the DRAW phase! // Invalidates Composition -> triggers recomposition loop! componentHeight = size.height.dp } ) }
Backwards: Reading in Composition, writing in Composition (same phase)
- چه اتفاقی میافتد: خواندن
countدر تابع ترکیبپذیر، و اصلاحcountمستقیماً در جایگاه محتوای ترکیبپذیر دیگر پساز خواندن آن. - نتیجه: سیستم عکس آنی خواندن و نوشتن بعدی را در همان پاس ترکیببندی ضبط میکند و بلافاصله محدوده فعلی را نامعتبر میکند.
// ❌ BAD: Direct write in Composable body after read @Composable fun BadCounter() { var count by remember { mutableIntStateOf(0) } Text("Count: $count") // State read in Composition Button(onClick = {}) { count++ // State write in Composition (Backwards write!) } } // Acceptable - but error-prone as someone may add a read before the write : Direct write in Composable body before read @Composable fun OkCounter() { var count by remember { mutableIntStateOf(0) } Button(onClick = {}) { count++ // State write in Composition } Text("Count: $count") // State read in Composition }
قوانین کلیدی برای جلوگیری از نوشتنهای معکوس
- اگر آن وضعیت در «ترکیببندی» خوانده میشود،
در
onGloballyPositioned،onSizeChanged،onPlaced،onLayoutRectChanged، یاLayoutModifierدر وضعیت ننویسید زیرا این کار باعث ایجاد مشکل درستی قاب اول میشود.- اگر مختصات یا اندازههای چیدمان فقط برای طراحی سفارشی موردنیاز است،
آنها را مستقیماً در مرحله Draw یا Layout بخوانید (برای مثال، بااستفاده از
Modifier.drawWithCacheیاModifier.layout). - برای اندازهبندی در سطح پنجره (
WindowWidthSizeClass): مشاهده اندازه را به سطح پنجره ارتقا دهید. ترکیببندی قبلاز انجام اندازهگیری محلی، روی کلاسهای اندازه پنجره منشعب میشود. - ترکیببندی را یکنواخت نگه دارید: از یک «چیدمان» سفارشی یا مؤلفههایی
مثل
FlowRowیاLazyVerticalGridاستفاده کنید که اندازهگیری و جایگذاری را درطول «فاز ۲» بدون ترکیببندی مجدد یا تغییر دادن وضعیت استفادهشده در ترکیببندی تنظیم میکنند. - استفاده از زیرترکیببندی: از
BoxWithConstraintsیاSubcomposeLayoutاستفاده کنید وقتیکه ترکیبشدنیهای فرزند باید براساس عرض یا ارتفاع محلی منشعب شوند. احتیاط کنید: «زیرترکیب» هزینه عملکردی دارد و معمولاً میتوان از آن اجتناب کرد. - بهعنوان آخرین راهکار: اجازه دهید فریم اول اشتباه باشد، اندازه را در
onSizeChangedذخیره کنید تا بازترکیب فریم دوم را راهاندازی کنید. این امر باعث پرش چیدمان قابلمشاهده، لرزش، و خطر حلقههای بینهایت میشود.
- اگر مختصات یا اندازههای چیدمان فقط برای طراحی سفارشی موردنیاز است،
آنها را مستقیماً در مرحله Draw یا Layout بخوانید (برای مثال، بااستفاده از
- وضعیت را پساز اولین خواندن در ترکیببندی تغییر ندهید:
- اگرچه میتوانید با خیال راحت در اشیاء
MutableStateدرطول ترکیببندی خارج ازSideEffectبنویسید، اما دقت ویژهای داشته باشید تا مطمئن شوید در وضعیتی که ممکن است قبلاً در ترکیببندی خوانده باشید نمینویسید. توصیه میشود وقتی نیاز به نوشتن وضعیت در ترکیببندی پیش میآید، ازrememberUpdatedStateاستفاده کنید. نوشتن در یک حالت درطول ترکیببندی به روشی دیگر معمولاً نشانهای از یک اثر ازدسترفته یا یک حالت یا ترکیبشدنی نامناسب طراحیشده است. بهیاد داشته باشید که ترکیببندی خوشبینانه است و همیشه با جدیدترین مقدار یک حالت اجرا میشود، بنابراین ممکن است همه تغییرات حالت را در ترکیببندیهای مجدد نبینید. بهروزرسانیهای واسط کاربر نباید بهعنوان روشی برای مدیریت رویدادهای یکباره استفاده شود، که باعث میشود غیرمعمول باشد که مقدار یک حالت درنتیجه بازترکیب نیاز به بهروزرسانی داشته باشد. - از تغییر دادن وضعیتهایی که خارج از ترکیببندی مشاهده میشوند (برای مثال، فیلدهای
ViewModelیا پرچمهایisVisible) خودداری کنید. نوشتنهای وضعیت که بر ترکیببندی تأثیر میگذارند باید در لامبداهای رویداد (onClick)، کوروتینها (LaunchedEffect)، یا عوارض جانبی (SideEffect) قرار گیرند.rememberUpdatedStateیک استثنا است زیرا برای تغییر وضعیت طراحی شده است که فقط در بدنه@Composableاستفاده میشود.
- اگرچه میتوانید با خیال راحت در اشیاء
- خواندن وضعیت را به آخرین فاز ممکن موکول کنید:
- خواندن حالتها در Draw (
Modifier.graphicsLayer { alpha = ... }) یا Layout (Modifier.offset { IntOffset(...) }) تضمین میکند که تغییرات فقط فاز ۲ یا ۳ را باطل میکنند و فاز ۱ (ترکیببندی) را کاملاً نادیده میگیرند.
- خواندن حالتها در Draw (