Compose یک فریم را در سه مرحله کاملاً مرتب و رو به جلو اجرا میکند:

- ترکیببندی : توابع
@Composableرا برای ساخت و بهروزرسانی درخت رابط کاربری اجرا میکند. - چیدمان : کودکان را اندازه گیری کرده و سپس آنها را قرار می دهد.
- ترسیم : دستورات ترسیم بوم را برای رندر کردن پیکسلها روی صفحه منتشر میکند.
هر زمان که یک State Compose در طول هر مرحله خوانده شود، Compose به طور خودکار وابستگی بین آن وضعیت و مرحله مربوطه را ثبت میکند .
چه چیزی باعث میشود یک نوشته «رو به عقب» باشد؟
نوشتن معکوس زمانی رخ میدهد که یک وضعیت در فاز بعدی (یا در یک دامنه پاییندست - به معنای یک دامنه قابل ترکیب که به ترتیب در همان مسیر ترکیب، دیرتر اجرا میشود) نسبت به جایی که خوانده شده است ، تغییر کند و Compose را مجبور به ترکیب مجدد با زمانبندی یک فاز قبلی یا قابل ترکیب برای اجرای مجدد کند. نوشتن معکوس یک حلقه ترکیب مجدد بهینهسازی نشده است.

پیامدهای نوشتن وارونه
نوشتنهای معکوس لزوماً چیز بدی نیستند و همیشه باعث خرابی نمیشوند، اما ناکارآمد هستند و میتوانند از چندین طریق به عملکرد برنامه آسیب بزنند:
- رندر فریم اضافی و فریمهای از دست رفته : نوشتن معکوس، Compose را مجبور به اجرای ترکیبهای تکراری در فریمهای متوالی میکند که باعث هدر رفتن منابع CPU و GPU و به طور بالقوه باعث ایجاد اختلال میشود.
- مشکلات صحت فریم اول : اگر کامپوننت شما برای تعیین ابعاد یا وضعیت نهایی خود نیاز به نوشتن معکوس داشته باشد، فریم اول با دادههای نامعتبر، پیشفرض یا نامشخص (مانند اندازه صفر یا آفست نادرست) رندر میشود. این امر باعث میشود هنگام رندر فریم دوم، ظاهر بصری یا چشمک زدن طرحبندی قابل مشاهده باشد.
- حلقههای ترکیببندی مجدد بینهایت : اگر تغییر حالت، اندازه طرحبندی را تغییر دهد و اندازهبندی طرحبندی به طور مداوم مقدار جدیدی را در حالت بنویسد، میتوانید خطر ایجاد یک حلقه فریم بینهایت را داشته باشید که در آن صفحه نمایش دائماً هر فریم را بدون تثبیت شدن، دوباره ترکیببندی میکند.
جریان رو به جلو در فازها
تغییرات وضعیت باید همیشه از طریق مراحل زیر به جلو جریان داشته باشند:
| مرحله خواندن | نوشتن متن | قابل قبوله؟ | چرا |
|---|---|---|---|
طرحبندی ( Modifier.offset { } ) | ترکیب | بله | ترکیب، حالت را بهروزرسانی میکند → طرحبندی آن را بعداً در همان فریم و بدون ترکیب مجدد میخواند. |
رسم ( graphicsLayer { } ، drawBehind { } ) | ترکیب | بله | کامپوزیشن وضعیت را بهروزرسانی میکند → در مرحله آخر، Draw آن را میخواند. کامپوزیشن و لیآوت کاملاً نادیده گرفته میشوند. |
| قرعه کشی | طرح بندی | بله | Layout وضعیت را بهروزرسانی میکند و draw آن را میخواند، جریان معتبر. |
| ترکیب | فراخوانی رویداد ( onClick ، onValueChange ) که باعث تغییر وضعیت میشود. توجه : فراخوانیهای طرحبندی به عنوان رویداد محسوب نمیشوند. | بله | یک رویداد، حالتی را که برای هدایت ترکیببندی استفاده میشود، تغییر میدهد. اگر این رویداد خارج از فریم (نه در ترکیببندی، طرحبندی یا ترسیم) اتفاق بیفتد، این معتبر است. |
| ترکیب | کوروتین ( LaunchedEffect ) | بله - با احتیاط | به صورت ناهمگام وضعیت را در پاسخ به چرخه حیات/رویدادها بهروزرسانی میکند. نوشتن از اثرات میتواند معتبر باشد، اما میتواند به لایهبندی وضعیت ناکارآمد اشاره داشته باشد. در صورت امکان باید از آنها اجتناب شود. |
| قرارگیری (در طرحبندی) | اندازه گیری (در طرح بندی) | بله | در طرحبندی، بهروزرسانی وضعیت و سپس خواندن آن وضعیت در جایگذاری قابل قبول است. |
| اندازه گیری (در طرح بندی) | قرارگیری (در طرحبندی) | نه - برعکس بنویس | نوشتن در حالتی که در حالت خواندن قرار دارد، باعث ایجاد حلقه اندازهگیری مجدد میشود. |
| ترکیب | طرحبندی ( onSizeChanged ، LayoutModifier ) | نه - برعکس بنویسید | طرحبندی، حلقه ترکیب → ترکیب مجدد را نامعتبر میکند. |
| ترکیب | Draw ( drawWithContent , Canvas ) | نه - برعکس بنویسید | رسم، حلقه ترکیب → ترکیب مجدد را نامعتبر میکند. |
ترکیب فازها: رو به عقب در مقابل رو به جلو
در ادامه نمونههایی از نوشتن معکوس در Compose و نحوهی حل آنها آمده است.
به عقب: خواندن در انشا، نوشتن در صفحهآرایی
- چه اتفاقی میافتد : کامپوزیشن،
componentHeightمیخواند تا مشخص کند چه رابط کاربریای را منتشر کند. بعداً در فریم، فاز Layout نماها را اندازهگیری یا جایگذاری میکند و مقدار جدیدی را درcomponentHeightمینویسد (برای مثال، با استفاده ازonSizeChanged،onGloballyPositionedیاLayoutModifierسفارشی). - نتیجه : تغییر
componentHeightدر Layout، مرحله Composition را که به تازگی تکمیل شده است، نامعتبر میکند. توجه داشته باشید کهonSizeChangedاندازه را پس از اتمام مرحله اندازهگیری layout گزارش میدهد. اگر مقدار state بهروزرسانیشده در مرحله بعدی تثبیت شود، recomposition ممکن است پس از یک فریم اضافی متوقف شود؛ با این حال، اگر مقدار جدید به تغییر اندازه ادامه دهد، منجر به یک حلقه فریم بینهایت میشود. علاوه بر این،onGloballyPositionedپس از هر دو layout و placement اجرا میشود و باعث میشود نوشتن state در داخل آن حتی بیشتر مستعد recomposition مداوم و حلقههای relayout در فریمهای متوالی باشد.
// ❌ 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) } } }
به عقب: خواندن در ترکیببندی، نوشتن در طراحی
- چه اتفاقی میافتد : حالت (State) در بدنهی Composable (مرحلهی Composition) خوانده میشود، اما در داخل
Modifier.drawWithContent،Modifier.drawBehindیاCanvas(مرحلهی Draw) تغییر میکند. - نتیجه : فاز رسم، حالت را تغییر میدهد → ترکیب نامعتبر میشود → حلقه بیپایان.
// ❌ 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 } ) }
به عقب: خواندن در انشا، نوشتن در انشا (در همان مرحله)
- چه اتفاقی میافتد : خواندن
countدر تابع composable و تغییر مستقیمcountدر یک اسلات محتوای composables دیگر پس از خواندن آن. - نتیجه : سیستم snapshot عملیات خواندن و نوشتن بعدی را در همان مسیر ترکیب ثبت میکند و بلافاصله دامنه فعلی را نامعتبر میسازد.
// ❌ 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 }
قوانین کلیدی برای جلوگیری از نوشتن معکوس
- اگر state در Composition خوانده میشود، در
onGloballyPositioned،onSizeChangedیاLayoutModifierچیزی برای state ننویسید، زیرا این کار باعث مشکل صحت فریم اول میشود.- اگر مختصات یا اندازههای طرحبندی فقط برای ترسیم سفارشی مورد نیاز هستند، آنها را مستقیماً در مرحله ترسیم یا طرحبندی بخوانید (برای مثال، با استفاده از
Modifier.drawWithCacheیاModifier.layout). - برای اندازهگیری در سطح پنجره (
WindowWidthSizeClass): مشاهدات اندازه را به سطح پنجره منتقل کنید. قبل از اینکه اندازهگیری محلی انجام شود، ترکیببندی روی کلاسهای اندازه پنجره انجام میشود. - حفظ یکنواختی ترکیببندی: از یک طرحبندی سفارشی واحد یا اجزایی مانند
FlowRowیاLazyVerticalGridاستفاده کنید که اندازهگیری و قرارگیری را در طول فاز ۲ بدون ترکیببندی مجدد یا تغییر حالت استفاده شده در ترکیببندی تنظیم میکنند. - استفاده از Sub-composition: زمانی که کامپوننتهای فرزند باید بر اساس عرض یا ارتفاع محلی شاخهبندی شوند، از
BoxWithConstraintsیاSubcomposeLayoutاستفاده کنید. احتیاط: Subcomposition هزینه عملکردی دارد و معمولاً میتوان از آن اجتناب کرد. - به عنوان آخرین راه حل: اجازه دهید فریم اول اشتباه باشد، اندازه را در
onSizeChangedذخیره کنید تا ترکیب فریم دوم را فعال کنید. این باعث میشود طرحبندی قابل مشاهده ظاهر شود، از کار بیفتد و خطر حلقههای بینهایت را به همراه داشته باشد.
- اگر مختصات یا اندازههای طرحبندی فقط برای ترسیم سفارشی مورد نیاز هستند، آنها را مستقیماً در مرحله ترسیم یا طرحبندی بخوانید (برای مثال، با استفاده از
- حالت را پس از اولین بار خواندن در ترکیب، تغییر ندهید :
- اگرچه میتوانید در طول ترکیب، خارج از یک
SideEffect، با خیال راحت در اشیاءMutableStateبنویسید، اما دقت ویژهای داشته باشید که در حالتی که قبلاً در ترکیب خواندهاید، ننویسید. توصیه میشود در مواقعی که نیاز به نوشتن در ترکیب وجود دارد،rememberUpdatedStateاستفاده کنید. نوشتن در یک حالت در طول ترکیب به روش دیگر معمولاً نشانهای از وجود یک اثر از دست رفته یا یک حالت یا ترکیب نامناسب طراحی شده است. به یاد داشته باشید که ترکیب خوشبینانه است و همیشه با آخرین مقدار یک حالت اجرا میشود، بنابراین ممکن است همه تغییرات حالت را در ترکیبهای مجدد مشاهده نکنید. بهروزرسانیهای رابط کاربری نباید به عنوان راهی برای مدیریت رویدادهای یکباره استفاده شوند، که این امر باعث میشود بهروزرسانی مقدار یک حالت در نتیجه ترکیب مجدد غیرمعمول باشد. - از تغییر حالتهایی که خارج از ترکیب مشاهده میشوند (مثلاً فیلدهای
ViewModelیا پرچمهایisVisible) خودداری کنید. State Writeهایی که بر ترکیب تأثیر میگذارند، متعلق به رویداد lambdas (onClick)، coroutineها (LaunchedEffect) یا عوارض جانبی (SideEffect) هستند.rememberUpdatedStateیک استثنا است زیرا برای تغییر حالتی طراحی شده است که فقط در بدنه@Composableاستفاده میشود.
- اگرچه میتوانید در طول ترکیب، خارج از یک
- خواندن وضعیت را به آخرین فاز ممکن موکول میکند :
- خواندن حالتها در Draw (
Modifier.graphicsLayer { alpha = ... }) یا Layout (Modifier.offset { IntOffset(...) }) تضمین میکند که تغییرات فقط فاز ۲ یا ۳ را نامعتبر میکنند و فاز ۱ (ترکیب) را به طور کامل نادیده میگیرند.
- خواندن حالتها در Draw (