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

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

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

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


কী কারণে কোনও লেখা "উল্টো" হয়?

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

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

ব্যাকওয়ার্ড রাইটের ফলাফল

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

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

ফেজের মাধ্যমে ফ্লো ফরওয়ার্ড করা

ফেজগুলির মধ্যে দিয়ে স্টেট পরিবর্তন সবসময় এগিয়ে যাওয়া উচিত:

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

ফেজ কম্বিনেশন: পিছনের দিকে বনাম সামনের দিকে

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

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

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