Jetpack Compose-কে Kotlin-এর চারপাশে তৈরি করা হয়েছে। কিছু ক্ষেত্রে, Kotlin বিশেষ ইডিয়ম প্রদান করে যা ভাল Compose কোড লেখা সহজ করে তোলে। আপনি যদি অন্য কোনও প্রোগ্রামিং ল্যাঙ্গুয়েজে চিন্তা করেন এবং মনে মনে সেই ল্যাঙ্গুয়েজকে Kotlin-এ অনুবাদ করেন, তাহলে আপনি Compose-এর কিছু শক্তি মিস করতে পারেন এবং আপনি ইডিওম্যাটিক্যালি-লিখিত Kotlin কোড বুঝতে অসুবিধা বোধ করতে পারেন। Kotlin-এর স্টাইল সম্পর্কে আরও পরিচিতি অর্জন করলে আপনি এই ধরনের সমস্যা এড়াতে পারবেন।
ডিফল্ট আর্গুমেন্ট
Kotlin ফাংশন লেখার সময়, আপনি ফাংশন আর্গুমেন্টের জন্য ডিফল্ট ভ্যালু নির্দিষ্ট করতে পারেন, যা কলার স্পষ্টভাবে সেইসব ভ্যালু পাস না করলে ব্যবহার করা হয়। এই ফিচার ওভারলোড করা ফাংশনের প্রয়োজনীয়তা কমায়।
যেমন, ধরুন আপনি এমন একটি ফাংশন লিখতে চান যা একটি বর্গক্ষেত্র আঁকে। সেই ফাংশনে একটিমাত্র প্রয়োজনীয় প্যারামিটার থাকতে পারে, 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) { }
Kotlin-এ, আপনি একটি ফাংশন লিখতে পারেন এবং আর্গুমেন্টের জন্য ডিফল্ট মান নির্দিষ্ট করতে পারেন:
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 প্যারামিটার উল্লেখ করার মাধ্যমে, আপনি ডকুমেন্ট করেন যে
অন্যান্য সব প্যারামিটারের জন্য, আপনি ডিফল্ট ভ্যালু ব্যবহার করতে চান। অন্যদিকে, দ্বিতীয় স্নিপেট থেকে বোঝা যায় যে আপনি অন্যান্য প্যারামিটারের জন্য স্পষ্টভাবে ভ্যালু সেট করতে চান, যদিও আপনার সেট করা ভ্যালু ফাংশনের জন্য ডিফল্ট ভ্যালু হিসেবে
সেট করা হয়।
উচ্চ-ক্রমের ফাংশন এবং ল্যামডা এক্সপ্রেশন
Kotlin-এ উচ্চ-ক্রমের ফাংশন কাজ করে, এই ফাংশনগুলি
প্যারামিটার হিসেবে অন্যান্য ফাংশন গ্রহণ করে। Compose এই পদ্ধতির উপর ভিত্তি করে তৈরি করা হয়েছে। যেমন, Button কম্পোজেবল ফাংশন একটি onClick
ল্যাম্বডা প্যারামিটার প্রদান করে। সেই প্যারামিটারের ভ্যালু হল একটি ফাংশন, যা ব্যবহারকারী ক্লিক করলে
বোতামটি কল করে:
Button( // ... onClick = myClickFunction ) // ...
উচ্চ-ক্রমের ফাংশন স্বাভাবিকভাবেই ল্যাম্বডা এক্সপ্রেশন, এক্সপ্রেশনের সাথে পেয়ার করে
যা একটি ফাংশনে মূল্যায়ন করে। আপনার যদি ফাংশনটি একবারই প্রয়োজন হয়, তাহলে হায়ার-অর্ডার ফাংশনে পাস করার জন্য আপনাকে এটি অন্য কোথাও সংজ্ঞায়িত করতে হবে না। পরিবর্তে,
আপনি ল্যাম্বডা এক্সপ্রেশন ব্যবহার করে সেখানেই ফাংশনটি সংজ্ঞায়িত করতে পারেন।
আগের উদাহরণে ধরে নেওয়া হয়েছে যে myClickFunction() অন্য কোথাও সংজ্ঞায়িত করা হয়েছে।
কিন্তু আপনি যদি শুধুমাত্র এখানে সেই ফাংশনটি ব্যবহার করেন, তাহলে ল্যাম্বডা এক্সপ্রেশনের সাথে ইনলাইন ফাংশনটি
সংজ্ঞায়িত করা সহজ:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
ট্রেলিং ল্যাম্বডা
Kotlin উচ্চ-ক্রমের ফাংশন কল করার জন্য একটি বিশেষ সিনট্যাক্স অফার করে যার শেষ প্যারামিটার হল একটি ল্যাম্বডা। আপনি যদি সেই প্যারামিটার হিসেবে ল্যাম্বডা এক্সপ্রেশন পাস করতে চান, তাহলে ট্রেলিং ল্যাম্বডা সিনট্যাক্স ব্যবহার করতে পারেন। ল্যাম্বডা এক্সপ্রেশনটি বন্ধনীর মধ্যে রাখার পরিবর্তে, আপনি এটি পরে রাখুন। Compose-এ এটি একটি সাধারণ পরিস্থিতি, তাই কোডটি কেমন দেখায় তার সাথে আপনাকে পরিচিত হতে হবে।
যেমন, সব লেআউটের শেষ প্যারামিটার, যেমন
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") }
এই সিনট্যাক্স Compose-এ খুবই সাধারণ, বিশেষ করে
Column-এর মতো লেআউট এলিমেন্টের জন্য। শেষ প্যারামিটারটি হল একটি ল্যাম্বডা এক্সপ্রেশন যা এলিমেন্টের চিলড্রেনকে সংজ্ঞায়িত করে এবং সেই চিলড্রেনকে ফাংশন কলের পরে বন্ধনীতে নির্দিষ্ট করা হয়।
স্কোপ ও রিসিভার
কিছু পদ্ধতি ও প্রপার্টি শুধুমাত্র নির্দিষ্ট স্কোপে উপলভ্য। সীমিত স্কোপ আপনাকে এমন জায়গায় কার্যকারিতা অফার করতে দেয় যেখানে এটি প্রয়োজন এবং ভুলবশত এমন জায়গায় সেই কার্যকারিতা ব্যবহার করা এড়িয়ে যেতে দেয় যেখানে এটি উপযুক্ত নয়।
Compose-এ ব্যবহার করা একটি উদাহরণ বিবেচনা করুন। আপনি Row লেআউট
কম্পোজেবল কল করলে, আপনার কন্টেন্ট ল্যাম্বডা অটোমেটিক 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) ) }
কিছু API ল্যাম্বডা গ্রহণ করে যেগুলি রিসিভার স্কোপ-এ কল করা হয়। প্যারামিটার ঘোষণার উপর ভিত্তি করে, অন্য কোথাও সংজ্ঞায়িত করা হয়েছে এমন প্রপার্টি এবং ফাংশনে সেই ল্যাম্বডার অ্যাক্সেস আছে:
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( /*...*/ /* ... ) } )
আরও তথ্যের জন্য, Kotlin ডকুমেন্টেশনে রিসিভার সহ ফাংশন লিটারেল দেখুন।
প্রতিনিধিত্ব করা প্রপার্টি
Kotlin-এ ডেলিগেট করা প্রপার্টি কাজ করে। এইসব প্রপার্টিকে
ফিল্ডের মতো করে কল করা হয়, তবে কোনও এক্সপ্রেশন মূল্যায়ন করে
ডায়নামিকভাবে এগুলির ভ্যালু নির্ধারণ করা হয়। আপনি এই প্রপার্টিগুলি 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
আপনি প্রায়ই Compose ফাংশনে এই ধরনের কোড দেখতে পাবেন:
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()-এর মতো
প্রয়োজনীয় ফাংশন সংজ্ঞায়িত করে। আপনি
ডেটা ক্লাস ডকুমেন্টেশন থেকে আরও তথ্য পাবেন।
সিঙ্গেলটন অবজেক্ট
Kotlin আপনাকে সিঙ্গলটন, ক্লাস ঘোষণা করতে দেয় যার সবসময় একটি এবং শুধুমাত্র একটি
ইনস্ট্যান্স থাকে। এইসব সিঙ্গলটন object কীওয়ার্ড দিয়ে ঘোষণা করা হয়।
Compose প্রায়ই এই ধরনের অবজেক্ট ব্যবহার করে। যেমন,
MaterialTheme-কে সিঙ্গলটন অবজেক্ট হিসেবে সংজ্ঞায়িত করা হয়;
MaterialTheme.colors, shapes ও typography প্রপার্টিতে বর্তমান থিমের
ভ্যালু থাকে।
টাইপ-সেফ বিল্ডার ও DSL
Kotlin, টাইপ-সেফ বিল্ডারের সাহায্যে ডোমেন-নির্দিষ্ট ভাষা (DSL) তৈরি করার অনুমতি দেয়। DSLs-এর সাহায্যে জটিল হায়ারার্কিক্যাল ডেটা স্ট্রাকচার আরও সহজে ম্যানেজ ও পড়া যায়।
Jetpack Compose কিছু API-এর জন্য DSL ব্যবহার করে, যেমন LazyRow এবং
LazyColumn।
@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 সম্পর্কে আরও জানুন।
Kotlin coroutines
Kotlin-এ ল্যাঙ্গুয়েজ লেভেলে Coroutines অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং সংক্রান্ত সহায়তা অফার করে। থ্রেড ব্লক না করেই Coroutines এক্সিকিউশন সাসপেন্ড করতে পারে। একটি রেসপন্সিভ UI সহজাতভাবে অ্যাসিঙ্ক্রোনাস এবং Jetpack Compose, কলব্যাক ব্যবহার করার পরিবর্তে API লেভেলে কোরুটিন গ্রহণ করে এই সমস্যার সমাধান করে।
Jetpack Compose এমন API অফার করে যা UI লেয়ারের মধ্যে coroutines ব্যবহার করা নিরাপদ করে তোলে।
rememberCoroutineScope ফাংশনটি
একটি CoroutineScope রিটার্ন করে যার সাহায্যে আপনি ইভেন্ট হ্যান্ডলারে
কোরুটিন তৈরি করতে এবং কম্পোজ সাসপেন্ড 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() } } ) { /* ... */ }
ডিফল্ট হিসেবে, Coroutines পরপর কোডের ব্লক এক্সিকিউট করে। একটি চলমান
কোরুটিন যা একটি সাসপেন্ড ফাংশনকে কল করে, সাসপেন্ড ফাংশন রিটার্ন না করা পর্যন্ত তার এক্সিকিউশন সাসপেন্ড করে। সাসপেন্ড ফাংশন এক্সিকিউশনকে অন্য 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() } } ) { /* ... */ }
Coroutines-এর সাহায্যে অ্যাসিঙ্ক্রোনাস API একত্রিত করা আরও সহজ হয়। নিচের
উদাহরণে, ব্যবহারকারী স্ক্রিনে ট্যাপ করলে কোনও এলিমেন্টের পজিশন অ্যানিমেট করার জন্য
আমরা pointerInput মডিফায়ারকে অ্যানিমেশন API-এর সাথে একত্রিত করি।
@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) ) }
কোরুটিন সম্পর্কে আরও জানতে, Android-এ Kotlin কোরুটিন নির্দেশিকা দেখুন।
আপনার জন্য সাজেস্ট করা হয়েছে
- মনে রাখবেন: জাভাস্ক্রিপ্ট বন্ধ থাকলে লিঙ্ক টেক্সট দেখানো হয়
- মেটেরিয়াল কম্পোনেন্ট ও লেআউট
- Compose-এ সাইড-এফেক্ট
- Compose লেআউটের প্রাথমিক তথ্য