কম্পোজ একটি ফ্রেমকে তিনটি কঠোরভাবে ক্রমবিন্যস্ত, সম্মুখ-প্রবাহী ধাপে সম্পাদন করে:

- কম্পোজিশন : UI ট্রি তৈরি ও আপডেট করার জন্য
@Composableফাংশনগুলো চালায়। - বিন্যাস : শিশুদের মাপ নিয়ে তাদের স্থান নির্ধারণ করে।
- অঙ্কন : স্ক্রিনে পিক্সেল রেন্ডার করার জন্য ক্যানভাস ড্র কমান্ড নির্গত করে।
যেকোনো ফেজ চলাকালীন যখনই কোনো কম্পোজ State পড়া হয়, কম্পোজ স্বয়ংক্রিয়ভাবে সেই স্টেট এবং সংশ্লিষ্ট ফেজের মধ্যে একটি নির্ভরতা রেকর্ড করে ।
কী কারণে লেখা 'উল্টো' হয়?
যখন কোনো স্টেটকে যেখান থেকে পড়া হয়েছিল তার চেয়ে পরবর্তী কোনো ফেজে (বা ডাউনস্ট্রিম স্কোপে—অর্থাৎ একই কম্পোজিশন পাসের মধ্যে ক্রমানুসারে পরে সম্পাদিত একটি কম্পোজেবল স্কোপে) পরিবর্তন করা হয় , তখন একটি ব্যাকওয়ার্ডস রাইট ঘটে। এর ফলে কম্পোজ পূর্ববর্তী কোনো ফেজ বা কম্পোজেবলকে পুনরায় চালানোর জন্য শিডিউল করে রিকম্পোজ করতে বাধ্য হয়। একটি ব্যাকওয়ার্ডস রাইট হলো একটি অপটিমাইজ না করা রিকম্পোজিশন লুপ।

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