সেরা পদ্ধতিগুলি অনুসরণ করুন

আপনি 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 রিকম্পোজ করা হলে, সম্পূর্ণ পরিচিতি তালিকা আবার সাজানো হয়, এমনকি তালিকা পরিবর্তন না হলেও। ব্যবহারকারী তালিকা স্ক্রল করলে, নতুন সারি দেখা গেলেই কম্পোজ করার উপযুক্ত আইটেমটি আবার কম্পোজ করা হয়।

এই সমস্যার সমাধান করতে, 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 প্রথমবার কম্পোজ করার সময় তালিকাটি একবার সাজানো হয়। পরিচিতি বা তুলনাকারী পরিবর্তন হলে, সাজানো তালিকা আবার তৈরি করা হয়। অন্যথায়, কম্পোজেবল ক্যাশে করা সাজানো তালিকা ব্যবহার করা চালিয়ে যেতে পারে।

remember ক্যালকুলেশন ব্লকের মধ্যে রেফারেন্স করা যেকোনও ইনপুট সবসময় কী হিসেবে পাস করুন যাতে সেইসব ইনপুট পরিবর্তন হলে ক্যাশে করা ভ্যালু আপডেট হয়। যেহেতু remember স্লট টেবিলে এর কী এবং ক্যাশে করা ফলাফল স্টোর করে এবং প্রতিটি রিকম্পোজিশন পাসে Object.equals() ব্যবহার করে কী তুলনা করে, তাই শুধুমাত্র ব্যয়বহুল গণনা বা ভারী অবজেক্ট বরাদ্দ (যেমন তারিখ ফরম্যাটার, শেডার বা তালিকা সাজানো) ক্যাশে করার সময় বা অবজেক্টের পরিচয় অবশ্যই সংরক্ষণ করতে হবে এমন ক্ষেত্রে remember ব্যবহার করুন।

সস্তা গণনার জন্য remember-এর 'কী' হিসেবে একটি বড় কালেকশন বা একটি অবজেক্টকে equals()-এর সাথে পাস করার সময় সতর্ক থাকুন, কারণ 'কী' তুলনা করার খরচ সরাসরি গণনা চালানোর চেয়ে বেশি হতে পারে:

@Composable
fun ContactHeader(contacts: List<Contact>) {
    // Avoid: Comparing the `contacts` list with .equals() on recomposition
    // is more expensive than reading contacts.size or contacts.firstOrNull() directly.
    val firstContactName = remember(contacts) {
        contacts.firstOrNull()?.name.orEmpty()
    }
    Text(firstContactName)
}

লেজি লেআউট কী ও কন্টেন্টের ধরন ব্যবহার করা

লেজি লেআউট আইটেমগুলি দক্ষতার সাথে পুনরায় ব্যবহার করে, শুধুমাত্র প্রয়োজন হলে সেগুলি পুনরায় তৈরি বা পুনরায় কম্পোজ করে। তবে, আপনি রিকম্পোজিশন ও স্লট আবার ব্যবহার করার জন্য লেজি লেআউট অপ্টিমাইজ করতে সাহায্য করতে পারেন।

ধরুন, ব্যবহারকারীর কোনও অ্যাকশনের কারণে তালিকায় কোনও আইটেম সরে গেছে। যেমন, ধরুন আপনি নোটের একটি তালিকা দেখান যা পরিবর্তনের সময় অনুসারে সাজানো হয়েছে এবং সবচেয়ে সম্প্রতি পরিবর্তিত নোটটি উপরে রয়েছে।

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

তবে এই কোডে কোনও সমস্যা আছে। ধরা যাক, নিচের নোটটি পরিবর্তন করা হয়েছে। এটি এখন সবচেয়ে সম্প্রতি পরিবর্তন করা নোট, তাই এটি তালিকার সবচেয়ে উপরে চলে যায় এবং অন্যান্য সব নোট এক ধাপ নিচে নেমে যায়।

আপনার সাহায্য ছাড়া, Compose বুঝতে পারে না যে অপরিবর্তিত আইটেমগুলি শুধু তালিকায় সরানো হচ্ছে। পরিবর্তে, Compose মনে করে যে পুরনো "আইটেম ২" মুছে ফেলা হয়েছে এবং আইটেম ৩, আইটেম ৪ এবং বাকি সব আইটেমের জন্য একটি নতুন আইটেম তৈরি করা হয়েছে। এর ফলে, Compose তালিকার প্রতিটি আইটেম রিকম্পোজ করে, যদিও সেগুলির মধ্যে মাত্র একটি আসলে পরিবর্তিত হয়েছে।

এখানে সমাধান হল আইটেম কী প্রদান করা। প্রতিটি আইটেমের জন্য একটি স্টেবল কী প্রদান করলে, 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)
        }
    }
}

লেজি লেআউট (LazyColumn, LazyRow, LazyGrid) নিয়ে কাজ করার সময়, নিম্নলিখিত নির্দেশিকাও মনে রাখবেন:

  • Bundle-saveable, অনন্য কী ব্যবহার করুন: কী অবশ্যই অনন্য, নির্ধারক এবং Android Bundle-এ সেভ করার যোগ্য হতে হবে কারণ লেজি লেআউট অভ্যন্তরীণভাবে rememberSaveable ব্যবহার করে। Int, Long বা String-এর মতো প্রিমিটিভ টাইপ ব্যবহার করুন এবং কী হিসেবে কখনও তালিকার ইন্ডেক্স বা hashCode() ব্যবহার করবেন না।
  • হেটেরোজেনাস তালিকার জন্য সবসময় contentType নির্দিষ্ট করুন: contentType = { it.type }-কে items বা itemsIndexed-এ পাস করুন যাতে স্ক্রল করার সময় একই ভিজ্যুয়াল স্ট্রাকচার শেয়ার করে এমন আইটেমের মধ্যে Compose শুধুমাত্র কম্পোজিশন স্লট আবার ব্যবহার করে।
  • indexOf-এর পরিবর্তে itemsIndexed ব্যবহার করুন: items ব্লকের মধ্যে list.indexOf(item) কল করা এড়িয়ে চলুন, যা লেআউটকে O(N²) স্ক্যানে পরিবর্তন করে; পরিবর্তে itemsIndexed ব্যবহার করুন।
  • শেয়ার করা স্টেট এবং CompositionLocal রিড হোইস্ট করা: শেয়ার করা Flow ইনস্ট্যান্স সংগ্রহ করুন এবং লেজি লেআউটের উপরে পেরেন্ট কম্পোজেবলে একবার CompositionLocal ভ্যালু (যেমন LocalContext.current বা LocalDensity.current) রিড করুন এবং সমাধান করা ভ্যালু আইটেম কম্পোজেবলে পাস করুন।
  • ছোট, বাউন্ডেড তালিকার জন্য Row বা Column ব্যবহার করুন: ছোট স্ট্যাটিক বা বাউন্ডেড সংগ্রহের (যেমন ১ থেকে ১০টি আইটেম) জন্য, forEach সহ স্ট্যান্ডার্ড Row বা Column ব্যবহার করলে লেজি ভার্চুয়ালাইজেশন ও প্রিফেচিং ওভারহেড এড়ানো যায়।

রিকম্পোজিশন সীমিত করতে computedStateOf বা derivedStateOf ব্যবহার করুন

আপনার কম্পোজিশনে State ব্যবহার করার একটি ঝুঁকি হল, State দ্রুত পরিবর্তিত হলে, আপনার UI আপনার প্রয়োজনের চেয়ে বেশি বার আবার কম্পোজ হতে পারে। যেমন, ধরুন আপনি স্ক্রল করা যায় এমন তালিকা দেখাচ্ছেন। তালিকার কোন আইটেমটি প্রথমে দেখা যাচ্ছে তা জানতে আপনি তালিকার অবস্থা পরীক্ষা করেন:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

এখানে সমস্যা হল, ব্যবহারকারী তালিকা স্ক্রল করলে, ব্যবহারকারী আঙুল দিয়ে টেনে নিয়ে যাওয়ার সাথে সাথে listState ক্রমাগত পরিবর্তিত হতে থাকে। এর অর্থ হল, তালিকাটি ক্রমাগত পুনর্গঠিত হচ্ছে। তবে, আপনাকে এটি প্রায়শই রিকম্পোজ করতে হবে না—নতুন আইটেম উপরে দেখা না যাওয়া পর্যন্ত আপনাকে রিকম্পোজ করতে হবে না। তাই, এটি অনেক বেশি অতিরিক্ত কম্পিউটেশন, যা আপনার UI-এর পারফর্ম্যান্স খারাপ করে দেয়।

সমাধান হল ড্রাইভড স্টেট (বা Compose Runtime 1.13 এবং তার পরবর্তী ভার্সনে computedStateOf) ব্যবহার করা। স্টেটের কোন পরিবর্তনগুলি আসলে রিকম্পোজিশন ট্রিগার করবে তা Derived state আপনাকে Compose-কে জানাতে দেয়। এই ক্ষেত্রে, প্রথম দৃশ্যমান আইটেম কখন পরিবর্তিত হয় সে সম্পর্কে আপনি যত্নশীল তা নির্দিষ্ট করুন। সেই স্টেট ভ্যালু পরিবর্তন হলে, UI-কে আবার কম্পোজ করতে হয়, কিন্তু ব্যবহারকারী যদি এখনও যথেষ্ট স্ক্রল না করে থাকেন যাতে নতুন আইটেম উপরে আসে, তাহলে এটিকে আবার কম্পোজ করতে হয় না।

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

আপনার Compose ভার্সন এবং ক্যালকুলেশন খরচের উপর ভিত্তি করে উপযুক্ত স্টেট ডেরিভেশন অ্যাপ্রোচ বেছে নিন:

  • remember { computedStateOf { ... } } (Compose Runtime 1.13 এবং তার পরবর্তী ভার্সন): ফলাফল ক্যাশে না করে প্রতিটি রিডে এর গণনা পুনরায় মূল্যায়ন করে এবং গণনা করা আউটপুট পরিবর্তিত হলেই কেবল কম্পোজিশন বাতিল করে। এটি ক্যাশে বুককিপিং এবং ইনভ্যালিডেশন রাইট-ব্যাক ওভারহেড এড়ায়, তাই দ্রুত পরিবর্তনশীল স্টেট (যেমন স্ক্রল অফসেট) দ্বারা চালিত প্রাথমিক কন্ডিশন চেক এবং লাইটওয়েট ডেরিভেশনের জন্য remember { computedStateOf { listState.firstVisibleItemIndex > 0 } } ব্যবহার করুন।
  • remember { derivedStateOf { ... } }: একাধিক রিড জুড়ে এর গণনার ফলাফল ক্যাশে করে। Compose-এর ১.১৩-এর আগের ভার্সনে derivedStateOf ব্যবহার করুন অথবা যখন ক্যালকুলেশন নিজেই কম্পিউটেশনালভাবে ব্যয়বহুল হয় এবং কম্পোজিশন পাসের সময় একাধিকবার রিড করা হয়। derivedStateOf-এর মধ্যে পড়া স্ন্যাপশট স্টেট ভ্যালু (যেমন listState.firstVisibleItemIndex) remember কী হিসেবে পাস করবেন না, কারণ এটি করলে প্রতিটি স্টেট পরিবর্তনের সময় DerivedState আবার তৈরি হয় এবং স্কিপ করা যায় না।
  • প্লেন Kotlin গেটার (get()) এবং সরাসরি প্রপার্টি রিড: তুচ্ছ বুলিয়ান এক্সপ্রেশনের জন্য একটি প্লেন কাস্টম গেটার পছন্দ করুন যেখানে ইনভ্যালিডেশন ড্যাম্পেনিংয়ের প্রয়োজন নেই এবং derivedStateOf-এ মোড়ানোর পরিবর্তে সরাসরি .size কালেকশন রিড করুন। সিঙ্ক্রোনাস স্টেট ডেরিভেশনের জন্য snapshotFlow-এর সাথে produceState ব্যবহার করা এড়িয়ে চলুন, কারণ এটি অপ্রয়োজনীয় কোরুটিন ডিসপ্যাচ ওভারহেড যোগ করে।

যতক্ষণ সম্ভব রিড স্থগিত রাখুন

পারফর্ম্যান্স সংক্রান্ত সমস্যা শনাক্ত করা হলে, স্টেট রিড স্থগিত করা সাহায্য করতে পারে। স্টেট রিড ডিফার করলে, Compose যাতে রিকম্পোজিশনে ন্যূনতম সম্ভাব্য কোড আবার রান করে তা নিশ্চিত করা যাবে। যেমন, আপনার UI-তে যদি এমন স্টেট থাকে যা কম্পোজেবল ট্রিতে অনেক উপরে হোস্ট করা হয় এবং আপনি চাইল্ড কম্পোজেবলে স্টেটটি পড়েন, তাহলে আপনি ল্যাম্বডা ফাংশনে স্টেট রিড র‍্যাপ করতে পারেন। এটি করলে, শুধুমাত্র প্রয়োজন হলেই রিড অপারেশন করা হয়। রেফারেন্সের জন্য, Jetsnack স্যাম্পেল অ্যাপে প্রয়োগ করা দেখুন। Jetsnack এর বিস্তারিত স্ক্রিনে কোলাপ্সিং-টুলবারের মতো এফেক্ট প্রয়োগ করে। এই টেকনিক কেন কাজ করে তা বুঝতে, ব্লগ পোস্ট Jetpack Compose: Debugging Recomposition দেখুন।

এই এফেক্ট পেতে, Title কম্পোজেবলের স্ক্রল অফসেট প্রয়োজন যাতে Modifier ব্যবহার করে এটি নিজেকে অফসেট করতে পারে। অপ্টিমাইজ করার আগে Jetsnack কোডের একটি সরলীকৃত ভার্সন এখানে দেওয়া হল:

@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। এর অর্থ হল, ড্র ফেজের সময় শুধু কালার স্টেট রিড করা হয়। এর ফলে, Compose কম্পোজিশন ও লেআউট ফেজ সম্পূর্ণভাবে এড়িয়ে যেতে পারে—রঙ পরিবর্তন হলে, Compose সরাসরি ড্র ফেজে চলে যায়।

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>
}

এই কোডটি পূর্ববর্তী লাইনে পড়ার পরে কম্পোজেবলের শেষে গণনা আপডেট করে। আপনি এই কোডটি রান করলে দেখতে পাবেন যে বোতামে ক্লিক করার পরে, যা একটি রিকম্পোজিশন ঘটায়, কাউন্টারটি দ্রুত একটি ইনফিনিট লুপে বৃদ্ধি পায় কারণ Compose এই Composable-কে রিকম্পোজ করে, একটি স্টেট রিড দেখে যা পুরানো, এবং তাই আরেকটি রিকম্পোজিশন শিডিউল করে।

কম্পোজিশনে স্টেট কখনও না লিখে আপনি ব্যাকওয়ার্ড রাইট সম্পূর্ণভাবে এড়াতে পারবেন। সম্ভব হলে, কোনও ইভেন্টের উত্তরে সবসময় স্টেট-এ লিখুন এবং পূর্ববর্তী onClick উদাহরণের মতো ল্যাম্বডাতে লিখুন।

ব্যাকওয়ার্ড রাইট, রিকম্পোজিশন লুপ ও ফেজ কোঅর্ডিনেশন সম্পর্কে বিস্তারিত জানতে, Compose-এ ব্যাকওয়ার্ড রাইট দেখুন।

অতিরিক্ত রিসোর্স