CompositionLocal-এর সাথে লোকালি স্কোপ করা ডেটা

CompositionLocal হল এমন একটি টুল যা কম্পোজিশনের মাধ্যমে ডেটা পাস করে। এই পৃষ্ঠায়, আপনি CompositionLocal কী তা আরও বিস্তারিতভাবে জানতে পারবেন, কীভাবে নিজের CompositionLocal তৈরি করতে হয় এবং আপনার ব্যবহারের ক্ষেত্রে CompositionLocal একটি ভালো সমাধান কিনা তা জানতে পারবেন।

CompositionLocal-এর ভূমিকা

সাধারণত, Compose-এ, UI ট্রি-এর মাধ্যমে প্রতিটি কম্পোজ করার উপযুক্ত ফাংশনে প্যারামিটার হিসেবে ডেটা নিচের দিকে ফ্লো করে। এর ফলে কোনও কম্পোজ করার মতো আইটেমের ডিপেন্ডেন্সি স্পষ্ট হয়ে যায়। তবে, কালার বা টাইপ স্টাইলের মতো খুব ঘনঘন ও ব্যাপকভাবে ব্যবহৃত ডেটার ক্ষেত্রে এটি অসুবিধাজনক হতে পারে। নিচে দেওয়া উদাহরণ দেখুন:

@Composable
fun MyApp() {
    // Theme information tends to be defined near the root of the application
    val colors = colors()
}

// Some composable deep in the hierarchy
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        color = colors.onPrimary // ← need to access colors here
    )
}

বেশিরভাগ কম্পোজেবলকে এক্সপ্লিসিট প্যারামিটার ডিপেন্ডেন্সি হিসেবে কালার পাস করার প্রয়োজন নেই। এটি নিশ্চিত করতে, Compose CompositionLocal অফার করে, যা আপনাকে ট্রি-স্কোপড নামযুক্ত অবজেক্ট তৈরি করতে দেয়। এগুলি UI ট্রি-এর মাধ্যমে ডেটা ফ্লো করার জন্য ইমপ্লিসিট উপায় হিসেবে ব্যবহার করা যেতে পারে।

CompositionLocal এলিমেন্ট সাধারণত UI ট্রি-এর কোনও নির্দিষ্ট নোডে মান সহ প্রদান করা হয়। কম্পোজ করার উপযুক্ত ফাংশনে CompositionLocal প্যারামিটার হিসেবে ঘোষণা না করেই এর কম্পোজ করার উপযুক্ত ডিসেন্ডেন্টরা সেই ভ্যালু ব্যবহার করতে পারে।

গুরুত্বপূর্ণ শব্দ: এই নির্দেশিকায়, আমরা কম্পোজিশন, UI ট্রি এবং UI হায়ারার্কি শব্দগুলি ব্যবহার করেছি। অন্যান্য নির্দেশিকায় এগুলি পরিবর্তনযোগ্য হিসেবে ব্যবহার করা হলেও, এদের অর্থ আলাদা আলাদা:

  • কম্পোজিশন হল কম্পোজ করা যায় এমন ফাংশনের কল গ্রাফের রেকর্ড।
  • UI ট্রি বা UI হায়ারার্কি হল LayoutNode কম্পোজিশন প্রসেস দ্বারা কনস্ট্রাক্ট, আপডেট ও মেন্টেন করা ট্রি।

CompositionLocal হল Material থিম যা ব্যাকগ্রাউন্ডে ব্যবহার করে। MaterialTheme হল এমন একটি অবজেক্ট যা তিনটি CompositionLocal ইনস্ট্যান্স প্রদান করে: colorScheme, typography এবং shapes, যা আপনাকে কম্পোজিশনের যেকোনও ডিসেন্ডেন্ট পার্টে পরে সেগুলি রিট্রিভ করতে দেয়। বিশেষত, এগুলি হল LocalColorScheme, LocalShapes এবং LocalTypography প্রপার্টি যা আপনি MaterialTheme colorScheme, shapes এবং typography অ্যাট্রিবিউটের মাধ্যমে অ্যাক্সেস করতে পারবেন।

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colorScheme, typography, and shapes are available
        // in MaterialTheme's content lambda.

        // ... content here ...
    }
}

// Some composable deep in the hierarchy of MaterialTheme
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // `primary` is obtained from MaterialTheme's
        // LocalColors CompositionLocal
        color = MaterialTheme.colorScheme.primary
    )
}

CompositionLocal ইনস্ট্যান্স কম্পোজিশনের একটি অংশের মধ্যে সীমাবদ্ধ, তাই আপনি ট্রিয়ের বিভিন্ন লেভেলে আলাদা আলাদা ভ্যালু প্রদান করতে পারবেন। CompositionLocal-এর current ভ্যালু কম্পোজিশনের সেই অংশে পূর্বপুরুষের দেওয়া সবচেয়ে কাছাকাছি ভ্যালুর সাথে মিলে যায়।

CompositionLocal-এ নতুন ভ্যালু দিতে, এর provides ইনফিক্স ফাংশন ব্যবহার করুন, যা CompositionLocal কী-কে value-এর সাথে যুক্ত করে। CompositionLocalProvider CompositionLocal-এর current প্রপার্টি অ্যাক্সেস করার সময় CompositionLocalProvider-এর content ল্যাম্বডা প্রদত্ত ভ্যালু পাবে। নতুন ভ্যালু প্রদান করা হলে, কম্পোজিশনের সেইসব অংশ Compose আবার কম্পোজ করে যেগুলি CompositionLocal পড়ে।

এর একটি উদাহরণ হল, LocalContentColor CompositionLocal-এ টেক্সট ও আইকনোগ্রাফির জন্য পছন্দের কন্টেন্টের রঙ ব্যবহার করা হয়েছে যাতে তা বর্তমান ব্যাকগ্রাউন্ডের সাথে কনট্রাস্ট করে। নিচের উদাহরণে, কম্পোজিশনের বিভিন্ন অংশের জন্য আলাদা ভ্যালু প্রদান করতে CompositionLocalProvider ব্যবহার করা হয়েছে।

@Composable
fun CompositionLocalExample() {
    MaterialTheme {
        // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default
        // This is to automatically make text and other content contrast to the background
        // correctly.
        Surface {
            Column {
                Text("Uses Surface's provided content color")
                CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
                    Text("Primary color provided by LocalContentColor")
                    Text("This Text also uses primary as textColor")
                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
                        DescendantExample()
                    }
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the error color now")
}

CompositionLocalExample কম্পোজ করার উপযুক্ত আইটেমের প্রিভিউ।
ছবি ১. CompositionLocalExample কম্পোজ করার উপযুক্ত আইটেমের প্রিভিউ।

শেষের উদাহরণে, CompositionLocal ইনস্ট্যান্সগুলি Material composables-এর দ্বারা ইন্টার্নালি ব্যবহার করা হয়েছে। CompositionLocal-এর বর্তমান ভ্যালু অ্যাক্সেস করতে, এর current প্রপার্টি ব্যবহার করুন। নিম্নলিখিত উদাহরণে, Android অ্যাপে সাধারণত ব্যবহৃত LocalContext CompositionLocal-এর বর্তমান Context ভ্যালু টেক্সট ফর্ম্যাট করতে ব্যবহার করা হয়েছে:

@Composable
fun FruitText(fruitSize: Int) {
    // Get `resources` from the current value of LocalContext
    val resources = LocalContext.current.resources
    val fruitText = remember(resources, fruitSize) {
        resources.getQuantityString(R.plurals.fruit_title, fruitSize)
    }
    Text(text = fruitText)
}

আপনার নিজস্ব CompositionLocal তৈরি করুন

CompositionLocal হল কম্পোজিশনের মাধ্যমে পরোক্ষভাবে ডেটা পাস করার একটি টুল।

CompositionLocal ব্যবহার করার আরেকটি গুরুত্বপূর্ণ সিগন্যাল হল, প্যারামিটারটি ক্রস-কাটিং এবং ইমপ্লিমেন্টেশনের ইন্টারমিডিয়েট লেয়ারের এটির অস্তিত্ব সম্পর্কে সচেতন হওয়া উচিত নয়, কারণ সেইসব ইন্টারমিডিয়েট লেয়ারকে সচেতন করে তুললে কম্পোজ করার উপযোগিতা সীমিত হয়ে যাবে। যেমন, Android-এর অনুমতির জন্য কোয়েরি করা CompositionLocal-এর মাধ্যমে গোপনে করা হয়। মিডিয়া পিকার কম্পোজ করার উপযুক্ত API পরিবর্তন না করেই ডিভাইসে অনুমতি-সুরক্ষিত কন্টেন্ট অ্যাক্সেস করার জন্য নতুন কার্যকারিতা যোগ করতে পারে এবং মিডিয়া পিকারের কলকারীদের এনভায়রনমেন্ট থেকে ব্যবহৃত এই যোগ করা প্রসঙ্গ সম্পর্কে সচেতন হতে হবে।

তবে, CompositionLocal সবসময় সেরা সমাধান নয়। আমরা CompositionLocal-এর অতিরিক্ত ব্যবহারকে অনুৎসাহিত করি কারণ এর কিছু অসুবিধা আছে:

CompositionLocal কম্পোজ করার উপযুক্ত আইটেমের আচরণ সম্পর্কে যুক্তি দেওয়া কঠিন করে তোলে। এগুলি পরোক্ষ নির্ভরতা তৈরি করে বলে, এগুলি ব্যবহার করে এমন কম্পোজ করার মতো ফাংশনের কলকারীদের নিশ্চিত করতে হবে যে প্রতিটি CompositionLocal-এর জন্য একটি ভ্যালু পূরণ করা হয়েছে।

এছাড়াও, এই নির্ভরতার জন্য কোনও স্পষ্ট সোর্স অফ ট্রুথ নাও থাকতে পারে কারণ এটি কম্পোজিশনের যেকোনও অংশে মিউটেট করতে পারে। তাই, কোনও সমস্যা হলে অ্যাপ ডিবাগ করা আরও কঠিন হতে পারে কারণ current ভ্যালু কোথায় দেওয়া হয়েছে তা দেখতে আপনাকে কম্পোজিশন পর্যন্ত নেভিগেট করতে হবে। IDE-তে ব্যবহার খুঁজুন বা Compose লেআউট ইন্সপেক্টরের মতো টুল এই সমস্যা কমানোর জন্য যথেষ্ট তথ্য প্রদান করে।

CompositionLocal ব্যবহার করবেন কিনা সেই ব্যাপারে সিদ্ধান্ত নিন

কিছু নির্দিষ্ট শর্ত পূরণ হলে, আপনার ব্যবহারের ক্ষেত্রে CompositionLocal একটি ভালো সমাধান হতে পারে:

CompositionLocal-এর একটি ভাল ডিফল্ট ভ্যালু থাকতে হবে। কোনও ডিফল্ট ভ্যালু না থাকলে, আপনাকে অবশ্যই গ্যারান্টি দিতে হবে যে কোনও ডেভেলপারের পক্ষে এমন পরিস্থিতিতে পড়া অত্যন্ত কঠিন যেখানে CompositionLocal-এর জন্য কোনও ভ্যালু প্রদান করা হয়নি। ডিফল্ট ভ্যালু না দিলে, টেস্ট তৈরি করার সময় বা কম্পোজ করার উপযুক্ত আইটেমের প্রিভিউ দেখার সময় সমস্যা হতে পারে। কারণ, সেটিতে CompositionLocal সবসময় এটি স্পষ্টভাবে প্রদান করতে হবে।

ট্রি-স্কোপ বা সাব-হায়ারার্কি স্কোপ হিসেবে বিবেচিত হয় না এমন কনসেপ্টের জন্য CompositionLocal এড়িয়ে চলুন। CompositionLocal তখনই যুক্তিযুক্ত হয় যখন সেটি সম্ভাব্যভাবে যেকোনও বংশধর ব্যবহার করতে পারে, কয়েকজনের দ্বারা নয়।

আপনার ব্যবহারের ক্ষেত্রে এইসব প্রয়োজনীয়তা পূরণ না হলে, কোনও CompositionLocal তৈরি করার আগে বিবেচনা করার বিকল্প বিভাগটি দেখুন।

খারাপ প্র্যাক্টিসের একটি উদাহরণ হল, এমন একটি CompositionLocal তৈরি করা যা কোনও নির্দিষ্ট স্ক্রিনের ViewModel ধরে রাখে যাতে সেই স্ক্রিনের সমস্ত কম্পোজ করার উপযুক্ত আইটেম কিছু লজিক পারফর্ম করার জন্য ViewModel-এর রেফারেন্স পেতে পারে। এটি একটি খারাপ প্র্যাক্টিস কারণ নির্দিষ্ট UI ট্রি-এর নিচে থাকা সব কম্পোজ করার উপযুক্ত আইটেমকে ViewModel সম্পর্কে জানতে হবে না। কম্পোজ করার উপযুক্ত আইটেমকে শুধুমাত্র সেইসব তথ্য দেওয়া উচিত যেগুলি তার প্রয়োজন এবং তা এমন প্যাটার্ন মেনে করা উচিত যেখানে স্টেট নিচের দিকে ফ্লো করে এবং ইভেন্ট উপরের দিকে ফ্লো করে। এই পদ্ধতি আপনার কম্পোজ করার উপযুক্ত আইটেমকে আরও বেশি করে আবার ব্যবহারযোগ্য এবং পরীক্ষা করা সহজ করে তুলবে।

CompositionLocal তৈরি করুন

CompositionLocal তৈরি করার জন্য দুটি API আছে:

  • compositionLocalOf: আবার কম্পোজ করার সময় দেওয়া ভ্যালু পরিবর্তন করলে, শুধুমাত্র সেই কন্টেন্ট বাতিল হয়ে যায় যেটি current ভ্যালু পড়ে।

  • staticCompositionLocalOf: compositionLocalOf-এর মতো, staticCompositionLocalOf-এর রিড Compose ট্র্যাক করে না। মান পরিবর্তন করলে, content ল্যাম্বডার সম্পূর্ণ অংশটি আবার কম্পোজ করতে হয়, যেখানে CompositionLocal প্রদান করা হয়। এর পরিবর্তে, কম্পোজিশনে current মানটি যেখানে পড়া হয়, শুধু সেই জায়গাটি আবার কম্পোজ করতে হয় না।

CompositionLocal-এ দেওয়া মান যদি খুব কমই পরিবর্তিত হয় বা কখনও পরিবর্তিত না হয়, তাহলে পারফর্ম্যান্স সংক্রান্ত সুবিধা পেতে staticCompositionLocalOf ব্যবহার করুন।

যেমন, কোনও অ্যাপের ডিজাইন সিস্টেম, UI কম্পোনেন্টের জন্য শ্যাডো ব্যবহার করে কম্পোজ করার উপযুক্ত আইটেম উন্নত করার ব্যাপারে মতামত দিতে পারে। যেহেতু অ্যাপের জন্য আলাদা উচ্চতা UI ট্রি জুড়ে প্রোপাগেট করা উচিত, তাই আমরা একটি CompositionLocal ব্যবহার করি। সিস্টেম থিমের উপর ভিত্তি করে CompositionLocal ভ্যালু শর্তসাপেক্ষে পাওয়া যায় বলে আমরা compositionLocalOf API ব্যবহার করি:

// LocalElevations.kt file

data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)

// Define a CompositionLocal global object with a default
// This instance can be accessed by all composables in the app
val LocalElevations = compositionLocalOf { Elevations() }

CompositionLocal-এর জন্য ভ্যালু দিন

CompositionLocalProvider কম্পোজ করা যায় এমন বাইন্ড, প্রদত্ত হায়ারার্কির জন্য CompositionLocal ইনস্ট্যান্সের সাথে ভ্যালু বাইন্ড করে। CompositionLocal-এ নতুন ভ্যালু দিতে, নিম্নলিখিত provides ইনফিক্স ফাংশন ব্যবহার করুন যা CompositionLocal কী-কে value-এর সাথে যুক্ত করে:

// MyActivity.kt file

class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // Calculate elevations based on the system theme
            val elevations = if (isSystemInDarkTheme()) {
                Elevations(card = 1.dp, default = 1.dp)
            } else {
                Elevations(card = 0.dp, default = 0.dp)
            }

            // Bind elevation as the value for LocalElevations
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... Content goes here ...
                // This part of Composition will see the `elevations` instance
                // when accessing LocalElevations.current
            }
        }
    }
}

CompositionLocal ব্যবহার করা

CompositionLocal.current সেই CompositionLocal-এর জন্য ভ্যালু প্রদানকারী সবচেয়ে কাছের CompositionLocalProvider-এর দেওয়া ভ্যালু রিটার্ন করে:

@Composable
fun SomeComposable() {
    // Access the globally defined LocalElevations variable to get the
    // current Elevations in this part of the Composition
    MyCard(elevation = LocalElevations.current.card) {
        // Content
    }
}

বিবেচনা করার মতো বিকল্প

কিছু ব্যবহারের ক্ষেত্রে CompositionLocal অতিরিক্ত সমাধান হতে পারে। আপনার ব্যবহারের ক্ষেত্রে CompositionLocal ব্যবহার করা উচিত কিনা তা নির্ধারণ করা বিভাগে উল্লেখ করা মানদণ্ড পূরণ না হলে, আপনার ব্যবহারের ক্ষেত্রে অন্য কোনও সমাধান বেশি উপযুক্ত হতে পারে।

অনুপযুক্ত প্যারামিটার পাস করা

কম্পোজ করার উপযুক্ত আইটেমের ডিপেন্ডেন্সি সম্পর্কে স্পষ্ট ধারণা রাখা ভালো। আমরা সাজেস্ট করি যে কম্পোজেবলকে শুধুমাত্র তার যা প্রয়োজন তাই পাস করুন। কম্পোজ করার মতো আইটেমকে আলাদা করা ও আবার ব্যবহার করার জন্য উৎসাহিত করতে, প্রতিটি কম্পোজ করার মতো আইটেমে যতটা সম্ভব কম তথ্য রাখা উচিত।

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel.data)
}

// Don't pass the whole object! Just what the descendant needs.
// Also, don't  pass the ViewModel as an implicit dependency using
// a CompositionLocal.
@Composable
fun MyDescendant(myViewModel: MyViewModel) { /* ... */ }

// Pass only what the descendant needs
@Composable
fun MyDescendant(data: DataToDisplay) {
    // Display data
}

ইনভার্সন অফ কন্ট্রোল

কম্পোজ করার উপযুক্ত ফাংশনে অপ্রয়োজনীয় ডিপেন্ডেন্সি পাস করা এড়ানোর আরেকটি উপায় হল ইনভার্সন অফ কন্ট্রোল ব্যবহার করা। ডিসেন্ডেন্ট কিছু লজিক এক্সিকিউট করার জন্য ডিপেন্ডেন্সি নেওয়ার পরিবর্তে, পেরেন্ট তা করে।

নিচের উদাহরণে দেখুন, কীভাবে কোনও ডিসেন্ডেন্টকে কিছু ডেটা লোড করার জন্য অনুরোধ ট্রিগার করতে হয়:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

কেসের উপর নির্ভর করে, MyDescendant-এর অনেক দায়িত্ব থাকতে পারে। এছাড়াও, MyViewModel-কে একটি নির্ভরতা হিসেবে পাস করলে, MyDescendant-এর পুনরায় ব্যবহারযোগ্যতা কমে যায়, কারণ এগুলি এখন একসাথে যুক্ত হয়ে গেছে। এমন বিকল্প বিবেচনা করুন যা ডিসেন্ডেন্টকে ডিপেন্ডেন্সি পাস করে না এবং ইনভার্সন অফ কন্ট্রোল নীতি ব্যবহার করে যা লজিক এক্সিকিউট করার জন্য পূর্বপুরুষকে দায়ী করে:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

এই পদ্ধতিটি কিছু ব্যবহারের ক্ষেত্রে আরও উপযুক্ত হতে পারে কারণ এটি চাইল্ডকে তার নিকটতম পূর্বপুরুষের থেকে আলাদা করে দেয়। আরও বেশি নমনীয় লোয়ার-লেভেল কম্পোজ করার উপযুক্ত হওয়ার জন্য অ্যান্সেস্টর কম্পোজ করার উপযুক্ত আইটেম আরও জটিল হয়ে যায়।

একইভাবে, @Composable কন্টেন্ট ল্যাম্বডাকে একই সুবিধা পেতে একইভাবে ব্যবহার করা যেতে পারে:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}