تم تصميم 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 فقط، أنت توثّق أنّك تريد استخدام القيم التلقائية لجميع المعلمات الأخرى. في المقابل، يشير مقتطف الرمز البرمجي الثاني إلى أنّك تريد ضبط القيم بشكلٍ صريح لتلك المعلمات الأخرى، على الرغم من أنّ القيم التي تضبطها هي القيم التلقائية للدالة.
الدوال ذات الترتيب الأعلى وتعبيرات lambda
تتيح لغة Kotlin الدوال ذات الترتيب الأعلى
، وهي الدوال التي تتلقى دوال أخرى كمعلمات. تستند Compose إلى هذا النهج. على
سبيل
Button
المثال، توفّر الدالة المركّبة مَعلمة lambda باسم onClick. قيمة هذه المَعلمة هي دالة يستدعيها الزر عندما ينقر عليها المستخدم:
Button( // ... onClick = myClickFunction ) // ...
تتكامل الدوال ذات الترتيب الأعلى بشكلٍ طبيعي مع تعبيرات lambda، وهي تعبيرات
تؤدي إلى دالة. إذا كنت بحاجة إلى الدالة مرة واحدة فقط، ليس عليك تعريفها في مكان آخر لتمريرها إلى الدالة ذات الترتيب الأعلى. بدلاً من ذلك، يمكنك تعريف الدالة في المكان نفسه باستخدام تعبير lambda. يفترض المثال السابق أنّ myClickFunction() معرَّفة في مكان آخر. ولكن إذا كنت تستخدم هذه الدالة هنا فقط، فمن الأسهل تعريف الدالة مضمّنة باستخدام تعبير lambda:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
تعبيرات lambda اللاحقة
توفّر لغة Kotlin بنية خاصة لاستدعاء الدوال ذات الترتيب الأعلى التي تكون آخر معلَمة فيها هي lambda. إذا أردت تمرير تعبير lambda كمعلَمة، يمكنك استخدام بنية lambda اللاحقة. بدلاً من وضع تعبير lambda بين قوسين، يمكنك وضعه بعد ذلك. هذه حالة شائعة في Compose، لذا عليك أن تكون على دراية بشكل الرمز البرمجي.
على سبيل المثال، آخر مَعلمة في جميع التنسيقات، مثل الـ
Column()
دالة المركّبة، هي content، وهي دالة تعرض عناصر واجهة المستخدم الثانوية. لنفترض أنّك تريد إنشاء عمود يحتوي على ثلاثة عناصر نصية، وعليك تطبيق بعض التنسيق. سيعمل هذا الرمز البرمجي، ولكنّه مرهق جدًا:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
بما أنّ المَعلمة content هي الأخيرة في توقيع الدالة، ونحن نمرِّر قيمتها كتعبير lambda، يمكننا إخراجها من الأقواس:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
للمثالَين المعنى نفسه تمامًا. تحدّد الأقواس تعبير lambda الذي يتم تمريره إلى المَعلمة content.
في الواقع، إذا كانت المَعلمة الوحيدة التي تمرِّرها هي تعبير lambda اللاحق، أي إذا كانت المَعلمة النهائية هي lambda، ولم تكن تمرِّر أي معلمات أخرى، يمكنك حذف الأقواس تمامًا. على سبيل المثال، لنفترض أنّك لم تكن بحاجة إلى تمرير معدِّل إلى Column. يمكنك كتابة الرمز البرمجي على النحو التالي:
Column { Text("Some text") Text("Some more text") Text("Last text") }
هذه البنية شائعة جدًا في Compose، خاصةً لعناصر التنسيق مثل Column. آخر مَعلمة هي تعبير lambda يحدّد العناصر الثانوية للعنصر، ويتم تحديد هذه العناصر الثانوية بين أقواس بعد استدعاء الدالة.
النطاقات والمستلِمون
لا تتوفّر بعض الطرق والخصائص إلا في نطاق معيّن. يتيح لك النطاق المحدود توفير الوظائف حيثما تكون مطلوبة وتجنُّب استخدام هذه الوظائف عن طريق الخطأ في الحالات غير المناسبة.
لنأخذ مثالاً مستخدَمًا في Compose. عند استدعاء الدالة المركّبة لتنسيق Rowlayout
، يتم استدعاء تعبير 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) ) }
تقبل بعض واجهات برمجة التطبيقات تعبيرات lambda التي يتم استدعاؤها في نطاق المستلِم. يمكن لتعبيرات lambda هذه الوصول إلى الخصائص والدوال المعرَّفة في مكان آخر، استنادًا إلى إعلان المَعلمة:
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(). يمكنك العثور على مزيد من المعلومات في مستندات فئات البيانات data
classes.
عناصر "سينغلتون"
تسهّل لغة Kotlin تعريف عناصر "سينغلتون" ، وهي فئات لا تتضمّن إلا مثيلاً واحدًا دائمًا و
فقط. يتم تعريف عناصر "سينغلتون" هذه باستخدام الكلمة الرئيسية object.
غالبًا ما تستخدم Compose هذه العناصر. على سبيل المثال،
MaterialTheme يتم
تعريفه كعنصر "سينغلتون". تحتوي الخصائص MaterialTheme.colors وshapes و
typography جميعها على قيم المظهر الحالي.
أدوات إنشاء آمنة من حيث النوع ولغات خاصة بالنطاق
تتيح لغة Kotlin إنشاء لغات خاصة بالنطاق (DSLs) باستخدام أدوات إنشاء آمنة من حيث النوع. تتيح لغات DSL إنشاء هياكل بيانات هرمية معقدة بطريقة أكثر قابلية للصيانة والقراءة.
يستخدم Jetpack Compose لغات 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) } } }
يمكنك التعرّف أكثر على أدوات الإنشاء الآمنة من حيث النوع ولغات DSL في مستندات Kotlin.
أنماط "كوروتين" في Kotlin
توفّر أنماط "كوروتين" دعمًا للبرمجة غير المتزامنة على مستوى اللغة في Kotlin. يمكن أن توقف أنماط "كوروتين" التنفيذ بدون حظر سلاسل المحادثات. إنّ واجهة المستخدم المتجاوبة غير متزامنة بطبيعتها، ويحلّ Jetpack Compose هذه المشكلة من خلال استخدام أنماط "كوروتين" على مستوى واجهة برمجة التطبيقات بدلاً من استخدام عمليات معاودة الاستدعاء.
توفّر Jetpack Compose واجهات برمجة تطبيقات تجعل استخدام أنماط "كوروتين" آمنًا ضمن طبقة واجهة المستخدم.
تعرض الدالة rememberCoroutineScope
عنصر CoroutineScope يمكنك من خلاله إنشاء أنماط "كوروتين" في معالِجات الأحداث واستدعاء
واجهات برمجة التطبيقات المعلقة في Compose. اطّلِع على المثال أدناه الذي يستخدم واجهة برمجة التطبيقات
ScrollState's
animateScrollTo.
// 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 مختلف. في المثال السابق، لن يتم تنفيذ loadData إلى أن تعرض الدالة المعلقة animateScrollTo قيمة.
لتنفيذ الرمز البرمجي بشكلٍ متزامن، يجب إنشاء أنماط "كوروتين" جديدة. في المثال أعلاه، لموازاة التمرير إلى أعلى الشاشة وتحميل البيانات من 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) ) }
لمزيد من المعلومات حول أنماط "كوروتين"، يمكنك الاطّلاع على دليل أنماط "كوروتين" في Kotlin على Android.
اقتراحات مخصصة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعّل
- مكوّنات Material والتنسيقات
- الآثار الجانبية في Compose
- أساسيات تنسيق Compose