نوشتن به‌عقب

‫Compose یک قاب را در سه مرحله با ترتیب دقیق و جریان رو به جلو اجرا می‌کند:

سه مرحله پیش‌رونده: ۱. قطعه موسیقی، ۲. چیدمان، ۳. طراحی
شکل ۱. سه مرحله قاب «نوشتن»
  1. ترکیب: @Composable تابع را برای ساختن و به‌روزرسانی درخت میانای کاربر اجرا می‌کند.
  2. چیدمان: کودکان را اندازه‌گیری می‌کند و سپس آن‌ها را قرار می‌دهد.
  3. ترسیم: فرمان‌های ترسیم بوم را برای پرداز کردن پیکسل‌ها در صفحه‌نمایش صادر می‌کند.

هرگاه یک «نگارش» State درطول هر مرحله‌ای خوانده شود، «نگارش» به‌طور خودکار وابستگی بین آن وضعیت و مرحله مربوطه را ثبت می‌کند.


چه چیزی باعث می‌شود که نوشتن «برعکس» باشد؟

نوشتن معکوس زمانی رخ می‌دهد که وضعیت در مرحله بعدی اصلاح شود (یا در محدوده پایین‌دست—یعنی محدوده ترکیب‌شدنی که بعداً در ترتیب در همان گذر ترکیب اجرا می‌شود) نسبت‌به جایی که خوانده شده است، که Compose را مجبور می‌کند با زمان‌بندی مرحله یا ترکیب‌شدنی قبلی برای اجرا شدن دوباره، دوباره ترکیب کند. بازنویسی یک حلقه بازسازی غیربهینه است.

مثالی از نوشتن معکوس ازطریق مراحل حلقه بازآرایی
شکل ۲. مثالی از نوشتن معکوس ازطریق حلقه ترکیب مجدد مراحل

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

نوشتن معکوس لزوماً چیز بدی نیست و همیشه باعث خرابی نمی‌شود، اما ناکارآمد است و می‌تواند به روش‌های مختلفی به عملکرد برنامه آسیب برساند:

  • پردازش قاب اضافی و قاب‌های رهاشده: نوشتن معکوس باعث می‌شود Compose گذرگاه‌های ترکیب اضافی را در قاب‌های متوالی اجرا کند، منابع واحد پردازش مرکزی و واحد پردازش گرافیکی را هدر دهد، و احتمالاً باعث پرش شود.
  • مشکلات صحت اولین قاب: اگر عنصر شما برای تعیین ابعاد یا وضعیت نهایی خود نیاز به نوشتن به عقب داشته باشد، اولین قاب با داده‌های نامعتبر، پیش‌فرض، یا نامشخص (مانند اندازه صفر یا افست نادرست) پرداز می‌شود. این کار باعث می‌شود وقتی قاب دوم پردازش می‌شود، جهش دیداری یا چشمک زدن چیدمان قابل‌مشاهده باشد.
  • حلقه‌های بازآرایی بی‌نهایت: اگر تغییر وضعیت باعث تغییر اندازه چیدمان شود، و اندازه چیدمان به‌طور مداوم مقدار جدیدی را در وضعیت بازنویسی کند، ممکن است حلقه قاب بی‌نهایتی ایجاد شود که در آن صفحه به‌طور مداوم هر قاب را بازآرایی می‌کند و هرگز پایدار نمی‌شود.

جریان روبه‌جلو ازطریق مراحل

تغییرات وضعیت همیشه باید ازطریق مراحل به‌جلو جریان داشته باشد:

خواندن فاز نوشتن بافت قابل‌قبول است؟ چرا
چیدمان (Modifier.offset { }) ترکیب بله «ترکیب» وضعیت را به‌روز می‌کند → «چیدمان» آن را بعداً در همان قاب بدون ترکیب مجدد می‌خواند.
تساوی (graphicsLayer { }، drawBehind { }) ترکیب بله «ترکیب» وضعیت را به‌روز می‌کند → «کشیدن» آن را در مرحله نهایی می‌خواند. «ترکیب» و «چیدمان» به‌طور کامل رد می‌شوند.
قرعه‌کشی چیدمان بله چیدمان وضعیت را به‌روز می‌کند و طراحی آن را می‌خواند، جریان معتبر.
ترکیب «بازخوان رویداد» (onClick، onValueChange) تغییر وضعیت را هدایت می‌کند. توجه: برگشتی‌های چیدمان به‌عنوان رویداد حساب نمی‌شوند. بله رویدادی وضعیت را که برای هدایت ترکیب استفاده می‌شود تغییر می‌دهد. اگر رویداد خارج از قاب (نه در «ترکیب»، «چیدمان»، یا «کشیدن») رخ دهد، این مورد معتبر است.
ترکیب روال همکار (LaunchedEffect) بله - با احتیاط وضعیت را به‌صورت ناهمزمان در پاسخ به چرخه حیات/رویدادها به‌روزرسانی می‌کند. نوشتن از جلوه‌ها می‌تواند معتبر باشد، اما می‌تواند به لایه‌بندی وضعیت ناکارآمد اشاره کند. درصورت امکان باید از آن‌ها اجتناب شود.
جای آگهی (در چیدمان) اندازه‌گیری (در چیدمان) بله در «چیدمان»، به‌روزرسانی وضعیت و سپس خواندن آن وضعیت در جای آگهی قابل‌قبول است.
اندازه‌گیری (در چیدمان) جای آگهی (در چیدمان) نه - نوشتن وارونه نوشتن برای بیان در جایگاهی که سپس در وضعیت خواندن بیشتر می‌شود، باعث حلقه اندازه‌گیری مجدد می‌شود.
ترکیب چیدمان (onSizeChanged، LayoutModifier) نه - نوشتن وارونه چیدمان «ترکیب» را نامعتبر می‌کند → حلقه «ترکیب مجدد».
ترکیب تساوی (drawWithContent، Canvas) نه - نوشتن معکوس «کشیدن» باعث نامعتبر شدن «ترکیب» می‌شود → حلقه «ترکیب مجدد».

ترکیب‌های فاز: عقب دربرابر جلو

در زیر نمونه‌هایی از نوشتن به عقب در «نوشتن» و نحوه رفع آن‌ها آورده شده است.

عقب: خواندن در «ترکیب»، نوشتن در «چیدمان»

  • چه اتفاقی می‌افتد: «ترکیب» 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)
        }
    }
}

عقب: خواندن در «قطعه موسیقی»، نوشتن در «طراحی»

  • چه اتفاقی می‌افتد: وضعیت در بدنه «ترکیب‌شدنی» (فاز «ترکیب») خوانده می‌شود، اما در 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
        }
    )
}

عقب‌گرد: خواندن در «ترکیب»، نوشتن در «ترکیب» (مرحله یکسان)

  • چه اتفاقی می‌افتد: خواندن 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
}


قوانین کلیدی برای جلوگیری از نوشتن به عقب

  1. برای بیان وضعیت در onGloballyPositioned، onSizeChanged، یا LayoutModifier ننویسید، زیرا اگر این وضعیت در «ترکیب» خوانده شود، باعث بروز مشکل صحت اولین قاب می‌شود.
    • اگر مختصات یا اندازه‌های چیدمان فقط برای طراحی سفارشی موردنیاز است، آن‌ها را مستقیماً در مرحله «طراحی» یا «چیدمان» بخوانید (برای مثال، بااستفاده از Modifier.drawWithCache یا Modifier.layout).
    • برای اندازه‌گیری سطح پنجره (WindowWidthSizeClass): اندازه بالابر مشاهده به سطح پنجره. شاخه‌های ترکیب براساس کلاس‌های اندازه پنجره قبل‌از انجام اندازه‌گیری محلی.
    • یکپارچه نگه داشتن ترکیب: از یک «چیدمان» سفارشی یا عناصر مانند FlowRow یا LazyVerticalGrid استفاده کنید که اندازه‌گیری و جای‌گذاری را درطول «فاز ۲» بدون بازترکیب کردن یا تغییر وضعیت استفاده‌شده در ترکیب تنظیم می‌کند.
    • استفاده از ترکیب فرعی: وقتی عناصر ترکیبی فرزند باید براساس عرض یا ارتفاع محلی شاخه‌بندی شوند، از BoxWithConstraints یا SubcomposeLayout استفاده کنید. با احتیاط استفاده کنید: زیرترکیب هزینه عملکرد دارد و معمولاً می‌توان از آن اجتناب کرد.
    • به‌عنوان آخرین راه حل: اجازه دهید قاب اول اشتباه باشد، اندازه را در onSizeChanged ذخیره کنید تا ترکیب مجدد قاب دوم را راه‌اندازی کنید. این کار باعث پرش چیدمان نمایان، لرزش، و خطر حلقه‌های بی‌نهایت می‌شود.
  2. وضعیت را پس‌از اولین خواندن آن در ترکیب تغییر ندهید:
    • اگرچه می‌توانید با ایمنی به MutableState شیء درطول ترکیب خارج از SideEffect بنویسید، مراقب باشید که به وضعیتی که قبلاً در ترکیب خوانده‌اید ننویسید. توصیه می‌شود وقتی این نیاز به نوشتن وضعیت در ترکیب پیش می‌آید از rememberUpdatedState استفاده کنید. نوشتن در وضعیت درحین ترکیب به روشی دیگر معمولاً نشانه‌ای از نبود جلوه یا وضعیت یا ترکیب‌شدنی طراحی‌نشده است. به‌خاطر داشته باشید که آهنگ‌سازی خوش‌بینانه است و همیشه با جدیدترین مقدار وضعیت اجرا می‌شود، بنابراین ممکن است همه تغییرات وضعیت را در آهنگ‌سازی‌های مجدد نبینید. به‌روزرسانی‌های واسط کاربر نباید به‌عنوان روشی برای مدیریت رویدادهای یک‌باره استفاده شود، که باعث می‌شود مقدار وضعیت به‌دلیل بازسازی نیاز به به‌روزرسانی داشته باشد.
    • از تغییر دادن وضعیت‌هایی که خارج از ترکیب مشاهده می‌شوند (برای مثال، ViewModel فیلدها یا isVisible پرچم‌ها) خودداری کنید. «State» می‌نویسد که ترکیب جلوه باید در لامبداهای رویداد (onClick)، روتین‌های همکار (LaunchedEffect)، یا جلوه‌های جانبی (SideEffect) قرار بگیرد. rememberUpdatedState استثنا است زیرا برای تغییر وضعیت طراحی شده است که فقط در @Composable بدنه استفاده می‌شود.
  3. خواندن وضعیت را به آخرین مرحله ممکن موکول کنید:
    • خواندن وضعیت‌ها در «طراحی» (Modifier.graphicsLayer { alpha = ... }) یا «چیدمان» (Modifier.offset { IntOffset(...) }) تضمین می‌کند که تغییرات فقط «فاز ۲» یا «فاز ۳» را نامعتبر می‌کند و «فاز ۱» (ترکیب) را به‌طور کامل رد می‌کند.