আপনি Compose-এর কিছু সাধারণ ভুলের সম্মুখীন হতে পারেন। এই ভুলগুলোর কারণে আপনার কোড আপাতদৃষ্টিতে ভালোভাবে চললেও, তা আপনার UI-এর পারফরম্যান্সের ক্ষতি করতে পারে। Compose-এ আপনার অ্যাপ অপ্টিমাইজ করার জন্য সেরা পদ্ধতিগুলো অনুসরণ করুন।
ব্যয়বহুল গণনা কমাতে remember
কম্পোজেবল ফাংশনগুলো খুব ঘন ঘন চলতে পারে , এমনকি একটি অ্যানিমেশনের প্রতিটি ফ্রেমের জন্যও। এই কারণে, আপনার কম্পোজেবলের বডিতে যতটা সম্ভব কম গণনা করা উচিত।
একটি গুরুত্বপূর্ণ কৌশল হলো remember ব্যবহার করে গণনার ফলাফল সংরক্ষণ করা । এর ফলে, গণনাটি একবারই সম্পন্ন হয় এবং প্রয়োজনের সময় আপনি ফলাফলগুলো সংগ্রহ করতে পারেন।
উদাহরণস্বরূপ, এখানে এমন একটি কোড রয়েছে যা নামের একটি সাজানো তালিকা প্রদর্শন করে, কিন্তু এটি সাজানোর কাজটি একটি অত্যন্ত ব্যয়বহুল পদ্ধতিতে করে থাকে:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // DON’T DO THIS items(contacts.sortedWith(comparator)) { contact -> // ... } } }
প্রতিবার ContactsList পুনর্গঠন করার সময়, সম্পূর্ণ কন্টাক্ট লিস্টটি আবার নতুন করে সাজানো হয়ে যায়, যদিও লিস্টটিতে কোনো পরিবর্তন হয় না। ব্যবহারকারী যদি লিস্টটি স্ক্রল করেন, তাহলে যখনই কোনো নতুন সারি আসে, Composable-টিও পুনর্গঠিত হয়ে যায়।
এই সমস্যাটি সমাধান করতে, LazyColumn এর বাইরে তালিকাটি সাজান, এবং সাজানো তালিকাটি remember করুন:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { val sortedContacts = remember(contacts, comparator) { contacts.sortedWith(comparator) } LazyColumn(modifier) { items(sortedContacts) { // ... } } }
এখন, ContactList প্রথমবার কম্পোজ করার সময় তালিকাটি একবার সর্ট করা হয়। যদি কন্ট্যাক্ট বা কম্প্যারেটর পরিবর্তিত হয়, তাহলে সর্ট করা তালিকাটি পুনরায় তৈরি করা হয়। অন্যথায়, কম্পোজেবলটি ক্যাশে করা সর্ট করা তালিকাটি ব্যবহার করতে পারে।
লেজি লেআউট কী ব্যবহার করুন
লেজি লেআউট দক্ষতার সাথে আইটেম পুনঃব্যবহার করে এবং শুধুমাত্র প্রয়োজন হলেই সেগুলোকে পুনরায় তৈরি বা পুনর্গঠন করে। তবে, আপনি পুনর্গঠনের জন্য লেজি লেআউটকে অপ্টিমাইজ করতে পারেন।
ধরা যাক, ব্যবহারকারীর কোনো কার্যকলাপের ফলে তালিকার কোনো একটি আইটেম স্থানান্তরিত হয়। উদাহরণস্বরূপ, ধরুন আপনি নোটের একটি তালিকা দেখাচ্ছেন যা পরিবর্তনের সময় অনুসারে সাজানো এবং সবচেয়ে সম্প্রতি পরিবর্তিত নোটটি তালিকার শীর্ষে রয়েছে।
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes ) { note -> NoteRow(note) } } }
তবে এই কোডটিতে একটি সমস্যা আছে। ধরুন, তালিকার সবচেয়ে নিচের নোটটি পরিবর্তন করা হলো। তখন এটিই হবে সবচেয়ে সম্প্রতি পরিবর্তিত নোট, তাই এটি তালিকার শীর্ষে চলে যাবে এবং বাকি সব নোট এক ধাপ করে নিচে নেমে যাবে।
আপনার সাহায্য ছাড়া, Compose বুঝতে পারে না যে তালিকায় অপরিবর্তিত আইটেমগুলো শুধু স্থানান্তরিত হচ্ছে। এর পরিবর্তে, Compose মনে করে যে পুরানো "আইটেম ২" মুছে ফেলা হয়েছে এবং আইটেম ৩, আইটেম ৪, এবং এভাবে নিচের সবগুলোর জন্য নতুন আইটেম তৈরি করা হয়েছে। এর ফলে, Compose তালিকার প্রতিটি আইটেমকে নতুন করে সাজিয়ে ফেলে, যদিও সেগুলোর মধ্যে কেবল একটিই আসলে পরিবর্তিত হয়েছে।
এর সমাধান হলো আইটেম কী (key) প্রদান করা। প্রতিটি আইটেমের জন্য একটি স্থিতিশীল কী প্রদান করলে Compose অপ্রয়োজনীয় পুনর্গঠন এড়াতে পারে। এক্ষেত্রে, Compose নির্ধারণ করতে পারে যে বর্তমানে ৩ নম্বর স্থানে থাকা আইটেমটিই সেই একই আইটেম যা আগে ২ নম্বর স্থানে ছিল। যেহেতু সেই আইটেমটির কোনো ডেটা পরিবর্তিত হয়নি, তাই Compose-কে এটি পুনর্গঠন করতে হয় না।
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes, key = { note -> // Return a stable, unique key for the note note.id } ) { note -> NoteRow(note) } } }
পুনর্গঠন সীমিত করতে derivedStateOf ব্যবহার করুন
আপনার কম্পোজিশনে স্টেট ব্যবহারের একটি ঝুঁকি হলো, যদি স্টেট দ্রুত পরিবর্তিত হয়, তাহলে আপনার UI প্রয়োজনের চেয়ে বেশিবার পুনর্গঠিত হতে পারে। উদাহরণস্বরূপ, ধরুন আপনি একটি স্ক্রোলযোগ্য তালিকা প্রদর্শন করছেন। তালিকার প্রথম দৃশ্যমান আইটেম কোনটি তা দেখতে আপনি তালিকাটির স্টেট পরীক্ষা করছেন:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton = listState.firstVisibleItemIndex > 0 AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
এখানে সমস্যাটি হলো, ব্যবহারকারী যখন তালিকাটি স্ক্রল করেন, তখন আঙুল টেনে নিয়ে যাওয়ার সাথে সাথে listState ক্রমাগত পরিবর্তিত হতে থাকে। এর মানে হলো, তালিকাটি অনবরত পুনর্গঠিত হচ্ছে। কিন্তু, আপনার আসলে এত ঘন ঘন এটি পুনর্গঠন করার প্রয়োজন নেই—যতক্ষণ না তালিকার নিচে একটি নতুন আইটেম দৃশ্যমান হচ্ছে, ততক্ষণ এটি পুনর্গঠন করার দরকার নেই। ফলে, এতে প্রচুর অতিরিক্ত গণনা হয়, যা আপনার UI-এর পারফরম্যান্সকে খারাপ করে দেয়।
এর সমাধান হলো ডিরাইভড স্টেট ব্যবহার করা। ডিরাইভড স্টেট আপনাকে কম্পোজকে বলে দিতে দেয় যে স্টেটের কোন পরিবর্তনগুলো আসলে রিকম্পোজিশন ট্রিগার করবে। এক্ষেত্রে, নির্দিষ্ট করে দিন যে প্রথম দৃশ্যমান আইটেমটি কখন পরিবর্তিত হয়, তা আপনার জন্য গুরুত্বপূর্ণ। যখন সেই স্টেটের মান পরিবর্তিত হবে, তখন UI-কে রিকম্পোজ করতে হবে, কিন্তু যদি ব্যবহারকারী নতুন কোনো আইটেমকে শীর্ষে আনার জন্য যথেষ্ট স্ক্রল না করে থাকেন, তবে রিকম্পোজ করার প্রয়োজন নেই।
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
যতদূর সম্ভব পড়া বিলম্বিত করুন
যখন কোনো পারফরম্যান্স সমস্যা চিহ্নিত করা হয়, তখন স্টেট রিড ডিফার করা সহায়ক হতে পারে। স্টেট রিড ডিফার করলে এটি নিশ্চিত হয় যে রিকম্পোজিশনের সময় Compose সর্বনিম্ন সম্ভাব্য কোড পুনরায় রান করবে। উদাহরণস্বরূপ, যদি আপনার UI-এর স্টেট কম্পোজেবল ট্রি-এর অনেক উপরে হোইস্ট করা থাকে এবং আপনি একটি চাইল্ড কম্পোজেবল থেকে স্টেটটি রিড করেন, তাহলে আপনি স্টেট রিডটিকে একটি ল্যাম্বডা ফাংশনের মধ্যে র্যাপ করতে পারেন। এটি করলে রিডটি কেবল তখনই ঘটে যখন এটির আসলেই প্রয়োজন হয়। রেফারেন্সের জন্য, Jetsnack স্যাম্পল অ্যাপের ইমপ্লিমেন্টেশনটি দেখুন। Jetsnack তার ডিটেইল স্ক্রিনে একটি কোলাপসিং-টুলবারের মতো ইফেক্ট প্রয়োগ করে। এই কৌশলটি কেন কাজ করে তা বুঝতে, Jetpack Compose: Debugging Recomposition ব্লগ পোস্টটি দেখুন।
এই প্রভাবটি অর্জন করতে, একটি Modifier ব্যবহার করে নিজেকে অফসেট করার জন্য Title কম্পোজেবলটির স্ক্রল অফসেট প্রয়োজন। অপ্টিমাইজেশন করার আগে জেটস্নাক কোডের একটি সরলীকৃত সংস্করণ নিচে দেওয়া হলো:
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack, scroll.value) // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scroll: Int) { // ... val offset = with(LocalDensity.current) { scroll.toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
যখন স্ক্রল স্টেট পরিবর্তিত হয়, Compose নিকটতম প্যারেন্ট রিকম্পোজিশন স্কোপটিকে বাতিল করে দেয়। এক্ষেত্রে, নিকটতম স্কোপটি হলো SnackDetail কম্পোজেবল। উল্লেখ্য যে, Box একটি ইনলাইন ফাংশন, এবং তাই এটি কোনো রিকম্পোজিশন স্কোপ নয়। সুতরাং Compose SnackDetail এবং SnackDetail ভেতরের যেকোনো কম্পোজেবলকে রিকম্পোজ করে। আপনি যদি আপনার কোড পরিবর্তন করে শুধু সেই স্টেটটি পড়েন যেখানে আপনি এটি ব্যবহার করছেন, তাহলে আপনি রিকম্পোজ করার জন্য প্রয়োজনীয় এলিমেন্টের সংখ্যা কমাতে পারবেন।
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack) { scroll.value } // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... val offset = with(LocalDensity.current) { scrollProvider().toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
স্ক্রল প্যারামিটারটি এখন একটি ল্যাম্বডা। এর মানে হলো, Title এখনও হোয়িস্টেড স্টেটকে রেফারেন্স করতে পারে, কিন্তু এর ভ্যালুটি শুধুমাত্র Title ভেতরেই রিড করা হয়, যেখানে এটির আসলে প্রয়োজন। ফলে, যখন স্ক্রল ভ্যালু পরিবর্তিত হয়, তখন নিকটতম রিকম্পোজিশন স্কোপটি হয় Title কম্পোজেবল—Compose-কে আর পুরো Box রিকম্পোজ করতে হয় না।
এটি একটি ভালো উন্নতি, কিন্তু আপনি আরও ভালো করতে পারেন! যদি আপনি শুধুমাত্র একটি কম্পোজেবল-এর রি-লেআউট বা রি-ড্র করার জন্য রিকম্পোজিশন ঘটান, তবে আপনার সন্দেহ হওয়া উচিত। এক্ষেত্রে, আপনি কেবল Title কম্পোজেবল-এর অফসেট পরিবর্তন করছেন, যা লেআউট পর্যায়েই করা যেত।
@Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { // ... } }
পূর্বে, কোডটিতে Modifier.offset(x: Dp, y: Dp) ব্যবহৃত হতো, যা অফসেটকে একটি প্যারামিটার হিসেবে গ্রহণ করত। মডিফায়ারের ল্যাম্বডা সংস্করণে পরিবর্তন করে, আপনি নিশ্চিত করতে পারেন যে ফাংশনটি লেআউট পর্যায়ে স্ক্রল স্টেট পড়বে। এর ফলে, যখন স্ক্রল স্টেট পরিবর্তিত হয়, Compose সম্পূর্ণভাবে কম্পোজিশন পর্যায়টি এড়িয়ে সরাসরি লেআউট পর্যায়ে চলে যেতে পারে। যখন আপনি মডিফায়ারে ঘন ঘন পরিবর্তনশীল স্টেট ভেরিয়েবল পাস করবেন, তখন সম্ভব হলে মডিফায়ারের ল্যাম্বডা সংস্করণ ব্যবহার করা উচিত।
এই পদ্ধতির আরেকটি উদাহরণ এখানে দেওয়া হলো। এই কোডটি এখনো অপ্টিমাইজ করা হয়নি:
// Here, assume animateColorBetween() is a function that swaps between // two colors val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .background(color) )
এখানে, বক্সটির ব্যাকগ্রাউন্ডের রঙ দুটি রঙের মধ্যে দ্রুত পরিবর্তিত হচ্ছে। ফলে এই অবস্থাটি খুব ঘন ঘন বদলাচ্ছে। এরপর কম্পোজেবলটি ব্যাকগ্রাউন্ড মডিফায়ারে এই অবস্থাটি পড়ে নেয়। এর ফলে, বক্সটিকে প্রতি ফ্রেমে পুনরায় কম্পোজ করতে হয়, কারণ প্রতি ফ্রেমে রঙটি পরিবর্তিত হচ্ছে।
এটি উন্নত করতে, একটি ল্যাম্বডা-ভিত্তিক মডিফায়ার ব্যবহার করুন—এই ক্ষেত্রে, drawBehind । এর মানে হলো, রঙের অবস্থা শুধুমাত্র ড্র ফেজের সময় পড়া হয়। ফলে, কম্পোজ কম্পোজিশন এবং লেআউট পর্যায়গুলো পুরোপুরি এড়িয়ে যেতে পারে—যখন রঙ পরিবর্তন হয়, কম্পোজ সরাসরি ড্র ফেজে চলে যায়।
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .drawBehind { drawRect(color) } )
উল্টো লেখা এড়িয়ে চলুন
Compose-এর একটি মূল ধারণা হলো, আপনি এমন কোনো স্টেটে লিখবেন না যা ইতিমধ্যেই পড়া হয়ে গেছে । যখন আপনি এমনটা করেন, তখন তাকে ব্যাকওয়ার্ডস রাইট বলা হয় এবং এর ফলে প্রতিটি ফ্রেমে অবিরামভাবে রিকম্পোজিশন ঘটতে পারে।
নিম্নলিখিত কম্পোজেবলটিতে এই ধরনের ভুলের একটি উদাহরণ দেখানো হয়েছে।
@Composable fun BadComposable() { var count by remember { mutableIntStateOf(0) } // Causes recomposition on click Button(onClick = { count++ }, Modifier.wrapContentSize()) { Text("Recompose") } Text("$count") count++ // Backwards write, writing to state after it has been read</b> }
এই কোডটি পূর্ববর্তী লাইনে কম্পোজেবলটি পড়ার পর, সেটির শেষে কাউন্ট আপডেট করে। আপনি যদি এই কোডটি চালান, তাহলে দেখবেন যে বাটনটিতে ক্লিক করার পর (যা একটি রিকম্পোজিশন ঘটায়), কাউন্টারটি একটি অসীম লুপে দ্রুত বাড়তে থাকে। কারণ কম্পোজ এই কম্পোজেবলটিকে রিকম্পোজ করার সময় দেখে যে পড়া স্টেটটি পুরোনো হয়ে গেছে, এবং তাই আরেকটি রিকম্পোজিশন শিডিউল করে।
কম্পোজিশনে স্টেটে কখনও না লিখে আপনি ব্যাকওয়ার্ড রাইট পুরোপুরি এড়াতে পারেন। যদি সম্ভব হয়, সর্বদা কোনো ইভেন্টের প্রতিক্রিয়ায় এবং পূর্ববর্তী onClick উদাহরণের মতো একটি ল্যাম্বডাতে স্টেটে লিখুন।
অতিরিক্ত সম্পদ
- অ্যাপ পারফরম্যান্স গাইড : অ্যান্ড্রয়েডের পারফরম্যান্স উন্নত করার জন্য সেরা পদ্ধতি, লাইব্রেরি এবং টুলস সম্পর্কে জানুন।
- পারফরম্যান্স পরিদর্শন করুন : অ্যাপের পারফরম্যান্স পরিদর্শন করুন।
- বেঞ্চমার্কিং : অ্যাপের পারফরম্যান্স বেঞ্চমার্ক করুন।
- অ্যাপ চালু করা : অ্যাপ চালু করার প্রক্রিয়া অপ্টিমাইজ করুন।
- বেসলাইন প্রোফাইল : বেসলাইন প্রোফাইলগুলো বুঝুন।
আপনার জন্য প্রস্তাবিত
- দ্রষ্টব্য: জাভাস্ক্রিপ্ট বন্ধ থাকলেও লিঙ্কের লেখা প্রদর্শিত হয়।
- স্টেট এবং জেটপ্যাক কম্পোজ
- গ্রাফিক্স মডিফায়ার
- রচনায় চিন্তা