تم إنشاء 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. عند استدعاء الدالة المركّبة للتنسيق Row، يتم تلقائيًا استدعاء دالة 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 التي يتم استدعاؤها في نطاق المُستلِم. ويمكن لهذه الدوال الوصول إلى الخصائص والدوال المحدّدة في مكان آخر، استنادًا إلى تعريف المَعلمة:
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(). يمكنك العثور على مزيد من المعلومات في مستندات فئات البيانات.
كائنات Singleton
تتيح لك لغة Kotlin تعريف الأنماط الفردية، وهي فئات تحتوي دائمًا على مثيل واحد فقط. يتم تعريف هذه الكائنات الفردية باستخدام الكلمة الرئيسية object.
غالبًا ما تستخدم ميزة "الإنشاء" هذه العناصر. على سبيل المثال، يتم تعريف MaterialTheme ككائن أحادي، وتحتوي السمات MaterialTheme.colors وshapes وtypography على قيم المظهر الحالي.
أدوات الإنشاء الآمنة من حيث النوع ولغات DSL
تتيح لغة Kotlin إنشاء لغات خاصة بالنطاق (DSL) باستخدام أدوات إنشاء تمنع أخطاء الكتابة. تسمح لغات 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. يمكنك الاطّلاع على المثال التالي
باستخدام واجهة برمجة التطبيقات animateScrollTo في ScrollState.
// 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