জেটপ্যাক কম্পোজ কোটলিনকে কেন্দ্র করে তৈরি। কিছু ক্ষেত্রে, কোটলিনে বিশেষ কিছু ইডিওম (idiom) রয়েছে যা ভালো কম্পোজ কোড লেখা সহজ করে তোলে। আপনি যদি অন্য কোনো প্রোগ্রামিং ভাষায় চিন্তা করেন এবং মানসিকভাবে সেই ভাষাটিকে কোটলিনে অনুবাদ করেন, তাহলে আপনি সম্ভবত কম্পোজের কিছু সুবিধা থেকে বঞ্চিত হবেন এবং ইডিওম অনুযায়ী লেখা কোটলিন কোড বুঝতে আপনার অসুবিধা হতে পারে। কোটলিনের শৈলীর সাথে আরও পরিচিতি আপনাকে এই সমস্যাগুলো এড়াতে সাহায্য করতে পারে।
ডিফল্ট আর্গুমেন্ট
যখন আপনি একটি কোটলিন ফাংশন লেখেন, তখন ফাংশন আর্গুমেন্টগুলোর জন্য ডিফল্ট মান নির্দিষ্ট করে দিতে পারেন, যা তখন ব্যবহৃত হয় যখন কলার সেই মানগুলো স্পষ্টভাবে পাস করে না। এই বৈশিষ্ট্যটি ওভারলোডেড ফাংশনের প্রয়োজনীয়তা কমিয়ে দেয়।
উদাহরণস্বরূপ, ধরুন আপনি এমন একটি ফাংশন লিখতে চান যা একটি বর্গক্ষেত্র আঁকবে। সেই ফাংশনটিতে sideLength নামে একটিমাত্র আবশ্যিক প্যারামিটার থাকতে পারে, যা প্রতিটি বাহুর দৈর্ঘ্য নির্দিষ্ট করে। এতে thickness , edgeColor ইত্যাদির মতো আরও কয়েকটি ঐচ্ছিক প্যারামিটারও থাকতে পারে; যদি কলকারী সেগুলো নির্দিষ্ট না করে, তাহলে ফাংশনটি ডিফল্ট মান ব্যবহার করে। অন্যান্য ভাষায়, আপনাকে হয়তো একাধিক ফাংশন লিখতে হতে পারে:
// We don't need to do this in Kotlin! void drawSquare(int sideLength) { } void drawSquare(int sideLength, int thickness) { } void drawSquare(int sideLength, int thickness, Color edgeColor) { }
কোটলিনে, আপনি একটিমাত্র ফাংশন লিখে আর্গুমেন্টগুলোর জন্য ডিফল্ট মান নির্দিষ্ট করে দিতে পারেন:
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
একাধিক অপ্রয়োজনীয় ফাংশন লেখার ঝামেলা থেকে আপনাকে বাঁচানোর পাশাপাশি, এই ফিচারটি আপনার কোডকে আরও সহজে পাঠযোগ্য করে তোলে। যদি কলার কোনো আর্গুমেন্টের জন্য কোনো মান নির্দিষ্ট না করে, তবে তা নির্দেশ করে যে তারা ডিফল্ট মানটি ব্যবহার করতে ইচ্ছুক। এছাড়াও, নামযুক্ত প্যারামিটারগুলো কী ঘটছে তা বোঝা অনেক সহজ করে তোলে। আপনি যদি কোডটি দেখেন এবং এই ধরনের একটি ফাংশন কল দেখতে পান, drawSquare() কোডটি পরীক্ষা না করে প্যারামিটারগুলোর অর্থ কী তা আপনি হয়তো বুঝতে পারবেন না:
drawSquare(30, 5, Color.Red);
এর বিপরীতে, এই কোডটি স্ব-ব্যাখ্যামূলক:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
বেশিরভাগ Compose লাইব্রেরি ডিফল্ট আর্গুমেন্ট ব্যবহার করে, এবং আপনার লেখা কম্পোজেবল ফাংশনগুলোর ক্ষেত্রেও একই কাজ করা একটি ভালো অভ্যাস। এই অভ্যাসটি আপনার কম্পোজেবলগুলোকে কাস্টমাইজযোগ্য করে তোলে, কিন্তু এর ডিফল্ট আচরণকে কল করাও সহজ রাখে। সুতরাং, উদাহরণস্বরূপ, আপনি এইরকম একটি সাধারণ টেক্সট এলিমেন্ট তৈরি করতে পারেন:
Text(text = "Hello, Android!")
ঐ কোডটির প্রভাব নিম্নলিখিত, আরও বিশদ কোডটির মতোই, যেখানে Text প্যারামিটারগুলোর আরও বেশি অংশ স্পষ্টভাবে সেট করা হয়েছে:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
প্রথম কোড স্নিপেটটি শুধু অনেক বেশি সরল ও পাঠযোগ্যই নয়, এটি স্ব-ব্যাখ্যামূলকও। শুধুমাত্র text প্যারামিটারটি নির্দিষ্ট করার মাধ্যমে আপনি বুঝিয়ে দেন যে, অন্য সব প্যারামিটারের জন্য আপনি ডিফল্ট মানগুলো ব্যবহার করতে চান। এর বিপরীতে, দ্বিতীয় স্নিপেটটি থেকে বোঝা যায় যে আপনি ওই অন্য প্যারামিটারগুলোর জন্য মানগুলো সুস্পষ্টভাবে নির্ধারণ করতে চান, যদিও আপনার নির্ধারিত মানগুলোই ফাংশনটির ডিফল্ট মান হয়ে থাকে।
উচ্চতর ক্রমের ফাংশন এবং ল্যাম্বডা এক্সপ্রেশন
কোটলিন হায়ার-অর্ডার ফাংশন সমর্থন করে, অর্থাৎ এমন ফাংশন যা প্যারামিটার হিসেবে অন্য ফাংশন গ্রহণ করে। কম্পোজ এই পদ্ধতির উপর ভিত্তি করে তৈরি। উদাহরণস্বরূপ, Button কম্পোজেবল ফাংশনটি একটি onClick ল্যাম্বডা প্যারামিটার প্রদান করে। সেই প্যারামিটারের মান হলো একটি ফাংশন, যা ব্যবহারকারী ক্লিক করলে বাটনটি কল করে:
Button( // ... onClick = myClickFunction ) // ...
হায়ার-অর্ডার ফাংশনগুলো ল্যাম্বডা এক্সপ্রেশনের সাথে স্বাভাবিকভাবেই যুক্ত হয়, যা একটি ফাংশনে ইভ্যালুয়েট হয়। যদি আপনার ফাংশনটি শুধুমাত্র একবার প্রয়োজন হয়, তবে হায়ার-অর্ডার ফাংশনে পাস করার জন্য এটিকে অন্য কোথাও সংজ্ঞায়িত করার প্রয়োজন নেই। এর পরিবর্তে, আপনি একটি ল্যাম্বডা এক্সপ্রেশন ব্যবহার করে ফাংশনটি সেখানেই সংজ্ঞায়িত করতে পারেন। পূর্ববর্তী উদাহরণটি ধরে নেয় যে myClickFunction() ফাংশনটি অন্য কোথাও সংজ্ঞায়িত করা আছে। কিন্তু যদি আপনি ফাংশনটি শুধুমাত্র এখানেই ব্যবহার করেন, তবে একটি ল্যাম্বডা এক্সপ্রেশন ব্যবহার করে ফাংশনটিকে ইনলাইনে সংজ্ঞায়িত করাই সহজতর:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
ট্রেইলিং ল্যাম্বডা
কোটলিনে এমন হায়ার-অর্ডার ফাংশন কল করার জন্য একটি বিশেষ সিনট্যাক্স রয়েছে, যার শেষ প্যারামিটারটি একটি ল্যাম্বডা। আপনি যদি সেই প্যারামিটার হিসেবে একটি ল্যাম্বডা এক্সপ্রেশন পাস করতে চান, তাহলে আপনি ট্রেইলিং ল্যাম্বডা সিনট্যাক্স ব্যবহার করতে পারেন। এক্ষেত্রে ল্যাম্বডা এক্সপ্রেশনটিকে বন্ধনীর ভেতরে রাখার পরিবর্তে, এটিকে পরে রাখতে হয়। কম্পোজে এটি একটি সাধারণ পরিস্থিতি, তাই কোডটি দেখতে কেমন হয় সে সম্পর্কে আপনার পরিচিতি থাকা প্রয়োজন।
উদাহরণস্বরূপ, Column() কম্পোজেবল ফাংশনের মতো সমস্ত লেআউটের শেষ প্যারামিটারটি হলো content , যা একটি ফাংশন এবং এটি চাইল্ড UI এলিমেন্টগুলোকে তৈরি করে। ধরুন, আপনি তিনটি টেক্সট এলিমেন্ট সম্বলিত একটি কলাম তৈরি করতে চান এবং তাতে কিছু ফরম্যাটিং প্রয়োগ করতে হবে। এই কোডটি কাজ করবে, কিন্তু এটি খুবই কষ্টসাধ্য:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
যেহেতু ফাংশন সিগনেচারে content প্যারামিটারটি একদম শেষে থাকে এবং আমরা এর মান একটি ল্যাম্বডা এক্সপ্রেশন হিসেবে পাস করছি, তাই আমরা এটিকে বন্ধনীর বাইরে নিয়ে আসতে পারি:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
উদাহরণ দুটির অর্থ হুবহু একই। বন্ধনীগুলো সেই ল্যাম্বডা এক্সপ্রেশনটিকে সংজ্ঞায়িত করে, যা content প্যারামিটারে পাঠানো হয়।
প্রকৃতপক্ষে, যদি আপনি শুধুমাত্র শেষের ল্যাম্বডাটিই প্যারামিটার হিসেবে পাস করেন—অর্থাৎ, যদি শেষ প্যারামিটারটি একটি ল্যাম্বডা হয় এবং আপনি অন্য কোনো প্যারামিটার পাস না করেন—তাহলে আপনি বন্ধনীগুলো পুরোপুরি বাদ দিতে পারেন। সুতরাং, উদাহরণস্বরূপ, ধরুন আপনার Column এ কোনো মডিফায়ার পাস করার প্রয়োজন নেই। আপনি কোডটি এভাবে লিখতে পারেন:
Column { Text("Some text") Text("Some more text") Text("Last text") }
এই সিনট্যাক্সটি কম্পোজে বেশ প্রচলিত, বিশেষ করে Column মতো লেআউট এলিমেন্টগুলোর জন্য। শেষ প্যারামিটারটি হলো একটি ল্যাম্বডা এক্সপ্রেশন যা এলিমেন্টটির চাইল্ডগুলোকে সংজ্ঞায়িত করে, এবং সেই চাইল্ডগুলোকে ফাংশন কলের পরে ব্র্যাকেটের মধ্যে নির্দিষ্ট করা হয়।
স্কোপ এবং রিসিভার
কিছু মেথড এবং প্রপার্টি শুধুমাত্র একটি নির্দিষ্ট স্কোপের মধ্যেই উপলব্ধ থাকে। এই সীমিত স্কোপ আপনাকে যেখানে প্রয়োজন সেখানেই কার্যকারিতা প্রদান করতে এবং যেখানে উপযুক্ত নয় সেখানে ভুলবশত সেই কার্যকারিতা ব্যবহার করা এড়াতে সাহায্য করে।
Compose-এ ব্যবহৃত একটি উদাহরণ বিবেচনা করুন। যখন আপনি Row লেআউটকে composable হিসেবে কল করেন, তখন আপনার content lambda স্বয়ংক্রিয়ভাবে একটি RowScope মধ্যে ইনভোক হয়। এটি Row এমন কার্যকারিতা প্রকাশ করতে সক্ষম করে যা শুধুমাত্র একটি Row মধ্যেই বৈধ। নীচের উদাহরণটি দেখায় কিভাবে Row align মডিফায়ারের জন্য একটি সারি-নির্দিষ্ট মান প্রকাশ করেছে:
Row { Text( text = "Hello world", // This Text is inside a RowScope so it has access to // Alignment.CenterVertically but not to // Alignment.CenterHorizontally, which would be available // in a ColumnScope. modifier = Modifier.align(Alignment.CenterVertically) ) }
কিছু এপিআই এমন ল্যাম্বডা গ্রহণ করে যেগুলোকে রিসিভার স্কোপে কল করা হয়। প্যারামিটার ডিক্লারেশনের উপর ভিত্তি করে, সেই ল্যাম্বডাগুলো অন্যত্র সংজ্ঞায়িত প্রোপার্টি এবং ফাংশন অ্যাক্সেস করতে পারে।
Box( modifier = Modifier.drawBehind { // This method accepts a lambda of type DrawScope.() -> Unit // therefore in this lambda we can access properties and functions // available from DrawScope, such as the `drawRectangle` function. drawRect( /*...*/ /* ... ) } )
আরও তথ্যের জন্য, কোটলিন ডকুমেন্টেশনে 'function literals with receiver' দেখুন।
অর্পিত সম্পত্তি
কোটলিন ডেলিগেটেড প্রপার্টি সমর্থন করে। এই প্রপার্টিগুলোকে ফিল্ডের মতোই কল করা হয়, কিন্তু একটি এক্সপ্রেশন ইভ্যালুয়েট করার মাধ্যমে এদের মান ডাইনামিকভাবে নির্ধারিত হয়। আপনি by সিনট্যাক্সের ব্যবহার দেখে এই প্রপার্টিগুলো চিনতে পারবেন:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
অন্যান্য কোড এইভাবে প্রপার্টিটি অ্যাক্সেস করতে পারে:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
যখন println() এক্সিকিউট হয়, তখন স্ট্রিংটির মান রিটার্ন করার জন্য nameGetterFunction() কল করা হয়।
এই ডেলিগেটেড প্রপার্টিগুলো বিশেষত তখন উপযোগী হয় যখন আপনি স্টেট-ব্যাকড প্রপার্টি নিয়ে কাজ করেন:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
ডেটা ক্লাসের কাঠামো ভেঙে ফেলা
যদি আপনি একটি ডেটা ক্লাস সংজ্ঞায়িত করেন, তাহলে একটি ডিস্ট্রাকচারিং ডিক্লারেশনের মাধ্যমে সহজেই ডেটা অ্যাক্সেস করতে পারেন। উদাহরণস্বরূপ, ধরুন আপনি একটি Person ক্লাস সংজ্ঞায়িত করেছেন:
data class Person(val name: String, val age: Int)
আপনার কাছে যদি ওই ধরনের কোনো অবজেক্ট থাকে, তাহলে আপনি এই ধরনের কোড দিয়ে তার মানগুলো অ্যাক্সেস করতে পারেন:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
কম্পোজ ফাংশনগুলোতে আপনি প্রায়শই এই ধরনের কোড দেখতে পাবেন:
Row { val (image, title, subtitle) = createRefs() // The `createRefs` function returns a data object; // the first three components are extracted into the // image, title, and subtitle variables. // ... }
ডেটা ক্লাস আরও অনেক দরকারি কার্যকারিতা প্রদান করে। উদাহরণস্বরূপ, যখন আপনি একটি ডেটা ক্লাস সংজ্ঞায়িত করেন, তখন কম্পাইলার স্বয়ংক্রিয়ভাবে equals() এবং copy() এর মতো দরকারি ফাংশনগুলো সংজ্ঞায়িত করে। আপনি ডেটা ক্লাসের ডকুমেন্টেশনে এ বিষয়ে আরও তথ্য পেতে পারেন।
একক বস্তু
কোটলিনে সিঙ্গেলটন ডিক্লেয়ার করা সহজ, যেগুলো এমন ক্লাস যার সবসময় কেবল একটিই ইনস্ট্যান্স থাকে। এই সিঙ্গেলটনগুলো object কীওয়ার্ড দিয়ে ডিক্লেয়ার করা হয়। `Compose` প্রায়শই এই ধরনের অবজেক্ট ব্যবহার করে। উদাহরণস্বরূপ, MaterialTheme একটি সিঙ্গেলটন অবজেক্ট হিসেবে সংজ্ঞায়িত; এর MaterialTheme.colors , shapes , এবং typography প্রোপার্টিগুলোতে বর্তমান থিমের ভ্যালুগুলো থাকে।
টাইপ-সেফ বিল্ডার এবং ডিএসএল
কোটলিন টাইপ-সেফ বিল্ডার ব্যবহার করে ডোমেইন-স্পেসিফিক ল্যাঙ্গুয়েজ (DSL) তৈরি করার সুযোগ দেয়। DSL-এর সাহায্যে জটিল স্তরক্রমিক ডেটা স্ট্রাকচার আরও সহজে রক্ষণাবেক্ষণযোগ্য ও পাঠযোগ্য উপায়ে তৈরি করা যায়।
Jetpack Compose কিছু API, যেমন LazyRow এবং LazyColumn জন্য DSL ব্যবহার করে।
@Composable fun MessageList(messages: List<Message>) { LazyColumn { // Add a single item as a header item { Text("Message List") } // Add list of messages items(messages) { message -> Message(message) } } }
Kotlin, রিসিভার সহ ফাংশন লিটারেল ব্যবহার করে টাইপ-সেফ বিল্ডার নিশ্চিত করে। উদাহরণস্বরূপ, Canvas কম্পোজেবলটি প্যারামিটার হিসেবে DrawScope রিসিভার সহ একটি ফাংশন গ্রহণ করে, যেমন onDraw: DrawScope.() -> Unit , যা কোডের ব্লকটিকে DrawScope এ সংজ্ঞায়িত মেম্বার ফাংশন কল করার সুযোগ দেয়।
Canvas(Modifier.size(120.dp)) { // Draw grey background, drawRect function is provided by the receiver drawRect(color = Color.Gray) // Inset content by 10 pixels on the left/right sides // and 12 by the top/bottom inset(10.0f, 12.0f) { val quadrantSize = size / 2.0f // Draw a rectangle within the inset bounds drawRect( size = quadrantSize, color = Color.Red ) rotate(45.0f) { drawRect(size = quadrantSize, color = Color.Blue) } } }
Kotlin-এর ডকুমেন্টেশনে টাইপ-সেফ বিল্ডার এবং DSL সম্পর্কে আরও জানুন।
কোটলিন কোরাউটিন
কোটলিনে কোরাউটিন ভাষা স্তরে অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং সমর্থন প্রদান করে। কোরাউটিন থ্রেড ব্লক না করেই এক্সিকিউশন স্থগিত করতে পারে। একটি রেসপন্সিভ UI স্বভাবতই অ্যাসিঙ্ক্রোনাস, এবং জেটপ্যাক কম্পোজ কলব্যাক ব্যবহার না করে এপিআই স্তরে কোরাউটিন গ্রহণ করার মাধ্যমে এর সমাধান করে।
Jetpack Compose এমন API প্রদান করে যা UI লেয়ারের মধ্যে কো-রুটিন ব্যবহারকে নিরাপদ করে তোলে। rememberCoroutineScope ফাংশনটি একটি ` CoroutineScope রিটার্ন করে, যা দিয়ে আপনি ইভেন্ট হ্যান্ডলারে কো-রুটিন তৈরি করতে এবং Compose-এর `suspend` API কল করতে পারেন। ` ScrollState এর animateScrollTo API ব্যবহার করে নিচের উদাহরণটি দেখুন।
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Create a new coroutine that scrolls to the top of the list // and call the ViewModel to load data composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }
কোরাউটিনগুলো ডিফল্টভাবে কোডের ব্লক ক্রমানুসারে সম্পাদন করে। একটি চলমান কোরাউটিন যখন কোনো সাসপেন্ড ফাংশনকে কল করে, তখন সেই ফাংশনটি রিটার্ন না করা পর্যন্ত এর এক্সিকিউশন স্থগিত থাকে । সাসপেন্ড ফাংশনটি এক্সিকিউশনকে অন্য কোনো CoroutineDispatcher এ স্থানান্তর করলেও এটি সত্য। পূর্ববর্তী উদাহরণে, সাসপেন্ড ফাংশন animateScrollTo রিটার্ন না করা পর্যন্ত loadData এক্সিকিউট হবে না।
একই সাথে একাধিক কোড চালানোর জন্য নতুন কো-রুটিন তৈরি করতে হয়। উপরের উদাহরণে, স্ক্রিনের শীর্ষে স্ক্রল করা এবং viewModel থেকে ডেটা লোড করার কাজ দুটিকে সমান্তরালভাবে চালানোর জন্য দুটি কো-রুটিনের প্রয়োজন।
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Scroll to the top and load data in parallel by creating a new // coroutine per independent work to do composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }
কো-রুটিন অ্যাসিঙ্ক্রোনাস এপিআইগুলোকে একত্রিত করা সহজ করে তোলে। নিচের উদাহরণে, ব্যবহারকারী স্ক্রিনে ট্যাপ করলে একটি এলিমেন্টের অবস্থান অ্যানিমেট করার জন্য আমরা pointerInput মডিফায়ারের সাথে অ্যানিমেশন এপিআইগুলোকে একত্রিত করেছি।
@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier .fillMaxSize() .pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen and animate // in the same block awaitPointerEventScope { val offset = awaitFirstDown().position // Launch a new coroutine to asynchronously animate to // where the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) }
Coroutines সম্পর্কে আরও জানতে, Android-এ Kotlin coroutines গাইডটি দেখুন।
{% হুবহু %}আপনার জন্য প্রস্তাবিত
- দ্রষ্টব্য: জাভাস্ক্রিপ্ট বন্ধ থাকলেও লিঙ্কের লেখা প্রদর্শিত হয়।
- উপাদান এবং বিন্যাস
- কম্পোজে পার্শ্ব-প্রতিক্রিয়া
- কম্পোজ লেআউটের মৌলিক বিষয়গুলি