উল্টোদিকে লেখা

Compose তিনটি কঠোরভাবে অর্ডার করা, ফরওয়ার্ড-ফ্লোয়িং ফেজে একটি ফ্রেম এক্সিকিউট করে:

তিনটি ফরওয়ার্ড-ফ্লোয়িং ফেজ: ১. কম্পোজিশন, ২. লেআউট, ৩. ড্রয়িং
ছবি ১. Compose ফ্রেমের তিনটি ফেজ
  1. কম্পোজিশন: UI ট্রি তৈরি ও আপডেট করতে @Composable ফাংশন রান করে।
  2. লেআউট: চিলড্রেন পরিমাপ করে এবং তারপর সেগুলি প্লেস করে।
  3. ড্রয়িং: স্ক্রিনে পিক্সেল রেন্ডার করার জন্য ক্যানভাস ড্র কমান্ড নির্গত করে।

কোনও ফেজ চলাকালীন Compose State পড়া হলে, Compose অটোমেটিক সেই স্ট্যাটাস ও সংশ্লিষ্ট ফেজের মধ্যে ডিপেন্ডেন্সি রেকর্ড করে।


What makes a write "backwards"?

যেখানে এটি পড়া হয়েছে তার থেকে পরবর্তী পর্যায়ে কোনও স্টেট পরিবর্তন করা হলে (অথবা ডাউনস্ট্রিম স্কোপে—অর্থাৎ একই কম্পোজিশন পাসের মধ্যে ক্রম অনুযায়ী পরে এক্সিকিউট করা কম্পোজেবল স্কোপ) ব্যাকওয়ার্ড রাইট হয়, যার ফলে Compose-কে আগের পর্যায় বা কম্পোজেবল আবার চালানোর জন্য শিডিউল করে রিকম্পোজ করতে হয়। ব্যাকওয়ার্ডস রাইট হল একটি অপ্টিমাইজ না করা রিকম্পোজিশন লুপ।

রিকম্পোজিশন লুপ ফেজের মাধ্যমে ব্যাকওয়ার্ড রাইটের উদাহরণ
ছবি ২. রিকম্পোজিশন লুপ ফেজের মাধ্যমে ব্যাকওয়ার্ড রাইটের উদাহরণ

পিছনের দিকে লেখার ফলাফল

ব্যাকওয়ার্ড রাইট যে সবসময় খারাপ তা নয় এবং এর ফলে সবসময় ক্র্যাশ হয় না, তবে এটি দক্ষ নয় এবং বিভিন্ন উপায়ে অ্যাপের পারফর্ম্যান্সের ক্ষতি করতে পারে:

  • অতিরিক্ত ফ্রেম রেন্ডারিং এবং ড্রপ করা ফ্রেম: ব্যাকওয়ার্ড রাইট, Compose-কে পরপর ফ্রেম জুড়ে অপ্রয়োজনীয় কম্পোজিশন পাস এক্সিকিউট করতে বাধ্য করে, যার ফলে CPU ও GPU রিসোর্স নষ্ট হয় এবং সম্ভাব্য জ্যাঙ্ক তৈরি হয়।
  • ফার্স্ট-ফ্রেম সঠিকতা সংক্রান্ত সমস্যা: আপনার কম্পোনেন্টের চূড়ান্ত ডাইমেনশন বা অবস্থা সমাধান করার জন্য যদি ব্যাকওয়ার্ড রাইটের প্রয়োজন হয়, তাহলে প্রথম ফ্রেমটি ভুল, ডিফল্ট বা অমীমাংসিত ডেটা (যেমন শূন্য সাইজ বা ভুল অফসেট) সহ রেন্ডার করা হয়। এর ফলে, দ্বিতীয় ফ্রেম রেন্ডার হওয়ার সময়, ভিজ্যুয়াল পপিং বা লেআউট ফ্ল্যাশিং দেখা যায়।
  • অসীম রিকম্পোজিশন লুপ: কোনও স্টেট পরিবর্তন যদি লেআউট সাইজ পরিবর্তন করে, এবং লেআউট সাইজ ক্রমাগত স্টেটে একটি নতুন ভ্যালু লিখে, তাহলে আপনি একটি অসীম ফ্রেম লুপ তৈরি করার ঝুঁকি নিতে পারেন যেখানে স্ক্রিন ক্রমাগত প্রতিটি ফ্রেমকে স্থিতিশীল না করেই রিকম্পোজ করে।

পর্যায়ের মাধ্যমে ফ্লো এগিয়ে নিয়ে যান

স্টেট পরিবর্তন সবসময় এই ফেজের মাধ্যমে এগিয়ে যেতে হবে:

পড়ার পর্যায় প্রসঙ্গ লিখুন গ্রহণযোগ্য? কেন
লেআউট (Modifier.offset { }) কম্পোজিশন হ্যাঁ কম্পোজিশন স্ট্যাটাস আপডেট করে → লেআউট পরে একই ফ্রেমে রিকম্পোজ না করেই এটি পড়ে।
ড্র (graphicsLayer { }, drawBehind { }) কম্পোজিশন হ্যাঁ কম্পোজিশন স্ট্যাটাস আপডেট করে → ড্র চূড়ান্ত পর্যায়ে এটি পড়ে। কম্পোজিশন ও লেআউট সম্পূর্ণভাবে এড়িয়ে যাওয়া হয়।
ড্র করা লেআউট হ্যাঁ লেআউট স্ট্যাটাস আপডেট করে এবং ড্র এটি পড়ে, সঠিক ফ্লো।
কম্পোজিশন ইভেন্ট কলব্যাক (onClick, onValueChange) ড্রাইভিং স্টেট পরিবর্তন। মনে রাখবেন: লেআউট কলব্যাক ইভেন্ট হিসেবে গণ্য করা হয় না। হ্যাঁ কোনও ইভেন্ট এমন স্টেটকে মিউটেট করে যা কম্পোজিশন চালাতে ব্যবহার করা হয়। ইভেন্টটি আউট-অফ-ফ্রেম (কম্পোজিশন, লেআউট বা ড্র-তে নয়) হলে, এটি সঠিক।
কম্পোজিশন Coroutine (LaunchedEffect) হ্যাঁ - সতর্কতার সাথে লাইফসাইকেল/ইভেন্টের উত্তরে অ্যাসিঙ্ক্রোনাসভাবে স্ট্যাটাস আপডেট করে। এফেক্ট থেকে রাইট করা বৈধ হতে পারে, তবে এটি অদক্ষ স্টেট লেয়ারিংয়ের দিকে ইঙ্গিত করতে পারে। সম্ভব হলে এগুলি এড়িয়ে চলুন।
প্লেসমেন্ট (লেআউটে) পরিমাপ (লেআউটে) হ্যাঁ লেআউটে, স্ট্যাটাস আপডেট করা এবং তারপরে প্লেসমেন্টে সেই স্ট্যাটাস রিড করা গ্রহণযোগ্য।
পরিমাপ (লেআউটে) প্লেসমেন্ট (লেআউটে) না - উল্টো করে লেখা প্লেসমেন্টে স্টেট লেখার পরে রিড স্টেটে আরও লেখার কারণে রিম্যাচ লুপ হয়।
কম্পোজিশন লেআউট (onSizeChanged, LayoutModifier) No - Backwards write লেআউট কম্পোজিশন বাতিল করে → রিকম্পোজিশন লুপ।
কম্পোজিশন ড্র (drawWithContent, Canvas) না - উল্টোদিকে লিখুন ড্র কম্পোজিশন বাতিল করে → রিকম্পোজিশন লুপ।

ফেজ কম্বিনেশন: ব্যাকওয়ার্ড বনাম ফরওয়ার্ড

Compose-এ ব্যাকওয়ার্ড রাইটের উদাহরণ এবং কীভাবে আপনি তা সমাধান করতে পারেন তা নিচে দেওয়া হল।

উল্টো: কম্পোজিশনে পড়া, লেআউটে লেখা

  • কী হয়: কোন UI নির্গত করতে হবে তা নির্ধারণ করতে কম্পোজিশন 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
        }
    )
}

ব্যাকওয়ার্ড: কম্পোজিশনে পড়া, কম্পোজিশনে লেখা (একই ফেজ)

  • কী হয়: কম্পোজেবল ফাংশনে 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, onPlaced, onLayoutRectChanged বা LayoutModifier-এ স্টেট লিখবেন না, যদি সেই স্টেট Composition-এ পড়া হয়, কারণ এটি ফার্স্ট-ফ্রেম-কারেক্টনেস সংক্রান্ত সমস্যা তৈরি করে।
    • কাস্টম ড্রয়িংয়ের জন্য লেআউট কোঅর্ডিনেট বা সাইজের প্রয়োজন হলে, সরাসরি ড্র বা লেআউট ফেজে সেগুলি পড়ুন (যেমন, Modifier.drawWithCache বা Modifier.layout ব্যবহার করে)।
    • উইন্ডো-লেভেল সাইজিংয়ের (WindowWidthSizeClass) জন্য: উইন্ডো লেভেলে সাইজ অবজারভেশন হোইস্ট করুন। লোকাল পরিমাপ হওয়ার আগে উইন্ডো সাইজ ক্লাসের উপর কম্পোজিশন ব্রাঞ্চ।
    • কম্পোজিশন একই রকম রাখুন: একটি কাস্টম লেআউট বা কম্পোনেন্ট যেমন FlowRow বা LazyVerticalGrid ব্যবহার করুন যা কম্পোজিশনে ব্যবহৃত স্টেট রিকম্পোজ বা পরিবর্তন না করেই ফেজ ২ চলাকালীন পরিমাপ এবং প্লেসমেন্ট অ্যাডজাস্ট করে।
    • সাব-কম্পোজিশন ব্যবহার করুন: BoxWithConstraints বা SubcomposeLayout ব্যবহার করুন যখন চাইল্ড কম্পোজেবলকে স্থানীয় প্রস্থ বা উচ্চতার ভিত্তিতে ব্রাঞ্চ করতে হবে। সতর্ক থাকুন: সাবকম্পোজিশন পারফর্ম্যান্সের খরচ বহন করে এবং সাধারণত এড়ানো যেতে পারে।
    • শেষ উপায় হিসেবে: প্রথম ফ্রেমটি ভুল হতে দিন, onSizeChanged-এ সাইজ স্টোর করুন যাতে দ্বিতীয় ফ্রেমের রিকম্পোজিশন ট্রিগার করা যায়। এর ফলে দৃশ্যমান লেআউট পপিং, জ্যাঙ্ক এবং অসীম লুপের ঝুঁকি তৈরি হয়।
  2. কম্পোজিশনে প্রথমবার স্টেট রিড করার পরে তা মিউটেট করবেন না:
    • SideEffect-এর বাইরে কম্পোজিশন চলাকালীন আপনি নিরাপদে MutableState অবজেক্টে লিখতে পারলেও, বিশেষ যত্ন নিন যাতে আপনি এমন কোনও স্টেটে না লেখেন যা আপনি আগে কম্পোজিশনে পড়ে থাকতে পারেন। কম্পোজিশনে স্টেট রাইট করার প্রয়োজন হলে rememberUpdatedState ব্যবহার করার সাজেশন দেওয়া হয়। কম্পোজিশনের সময় অন্য উপায়ে কোনও স্টেট লেখার অর্থ হল, সাধারণত কোনও এফেক্ট বাদ গেছে অথবা স্টেট বা কম্পোজেবল সঠিকভাবে ডিজাইন করা হয়নি। মনে রাখবেন যে কম্পোজিশন হল অপ্টিমিস্টিক এবং সবসময় কোনও স্টেট-এর লেটেস্ট ভ্যালুর মাধ্যমে এক্সিকিউট করা হয়, তাই আপনি হয়ত রিকম্পোজিশনে সব স্টেট-এর পরিবর্তন দেখতে পাবেন না। UI আপডেট এককালীন ইভেন্ট ম্যানেজ করার উপায় হিসেবে ব্যবহার করা উচিত নয়, যার ফলে রিকম্পোজ করার কারণে কোনও স্টেট ভ্যালু আপডেট করার প্রয়োজন হয় না।
    • কম্পোজিশনের বাইরে লক্ষ্য করা যায় এমন মিউটেটিং স্টেট এড়িয়ে চলুন (যেমন, ViewModel ফিল্ড বা isVisible ফ্ল্যাগ)। স্টেট রাইট যা কম্পোজিশনকে প্রভাবিত করে তা ইভেন্ট ল্যাম্বডা (onClick), কোরুটিন (LaunchedEffect) বা সাইড এফেক্টে (SideEffect) থাকে। rememberUpdatedState হল একটি ব্যতিক্রম কারণ এটি স্টেট মিউটেট করার জন্য ডিজাইন করা হয়েছে যা শুধুমাত্র @Composable বডিতে ব্যবহৃত হয়।
  3. সর্বশেষ সম্ভাব্য ফেজে স্টেট রিড ডিফার করা:
    • Draw (Modifier.graphicsLayer { alpha = ... }) বা Layout (Modifier.offset { IntOffset(...) })-এ স্টেট রিড করার মাধ্যমে নিশ্চিত করা হয় যে পরিবর্তনগুলি শুধুমাত্র ফেজ ২ বা ৩-কে বাতিল করে, ফেজ ১ (কম্পোজিশন) সম্পূর্ণভাবে এড়িয়ে যায়।