Kotlin لـ Jetpack Compose

تم إنشاء 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 استخدام الدوال ذات الترتيب الأعلى، وهي دوال تتلقّى دوال أخرى كمعلَمات. تستند ميزة "إنشاء" إلى هذا النهج. على سبيل المثال، توفّر الدالة المركّبة Button مَعلمة onClick lambda. قيمة هذه المَعلمة هي دالة يستدعيها الزر عندما ينقر المستخدم عليه:

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(
            /*...*/
            /* ...
        )
    }
)

لمزيد من المعلومات، يُرجى الاطّلاع على function literals with receiver في مستندات 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. يستخدم Compose غالبًا هذه العناصر. على سبيل المثال، يتم تعريف 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.