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 استخدام الدوال ذات الترتيب الأعلى، وهي الدوال التي تتلقّى دوال أخرى كمعلَمات. تستند 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. يستخدم 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. اطّلِع على المثال أدناه الذي يستخدم واجهة برمجة التطبيقات ScrollState 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()
        }
    }
) { /* ... */ }

تنفّذ إجراءات Coroutines مجموعة الرموز بالتسلسل تلقائيًا. يؤدي استدعاء دالة تعليق من خلال روتين فرعي قيد التشغيل إلى تعليق تنفيذ الروتين الفرعي إلى أن تعرض دالة التعليق قيمة. وينطبق ذلك حتى إذا نقلت وظيفة التعليق التنفيذ إلى 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
                        val offset = awaitPointerEventScope {
                            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.