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 مَعلمة LAMBDA‏ onClick. قيمة هذه المَعلمة هي دالة يستدعيها الزر عندما ينقر عليه المستخدم:

Button(
    // ...
    onClick = myClickFunction
)
// ...

ترتبط الدوالّ من الدرجة الأعلى بشكلٍ طبيعي بتعبيرات لامدا، وهي تعبيرات تُقيّم كدالة. إذا كنت بحاجة إلى الدالة مرة واحدة فقط، فلن تضطر إلى تحديدها في مكان آخر لتمريرها إلى الدالة ذات الترتيب الأعلى. بدلاً من ذلك، يمكنك تحديد الدالة مباشرةً باستخدام تعبير لامدا. يفترض المثال السابق أنّه تم تحديد myClickFunction() في مكان آخر. ولكن إذا كنت تستخدم هذه الدالة هنا فقط، من الأسهل تعريف الدالة مضمّنةً في تعبير lambda:

Button(
    // ...
    onClick = {
        // do something
        // do something else
    }
) { /* ... */ }

لامدا اللاحقة

تقدّم Kotlin بنية خاصة لاستدعاء الدوالّ من الدرجة الأعلى التي تكون المَعلمة الأخيرة فيها هي دالة LAMBDA. إذا أردت تمرير تعبير lambda كمَعلمة هذه، يمكنك استخدام بنية دالة lambda اللاحقة. بدلاً من وضع تعبير lambda بين الأقواس، ضعه بعد ذلك. هذا موقف شائع في ميزة "الإنشاء"، لذا عليك أن تكون على دراية بشكل الرمز.

على سبيل المثال، المعلمة الأخيرة لجميع التنسيقات، مثل دالة 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")
}

هذه البنية شائعة جدًا في ميزة "الإنشاء"، خاصةً لعناصر التنسيق مثل Column. المعلّمة الأخيرة هي تعبير لامبدا يحدّد عناصر العنصر، ويتم تحديد هذه العناصر بين قوسين بعد طلب الدالة.

النطاقات والمستلِمون

لا تتوفّر بعض الطرق والسمات إلا في نطاق معيّن. يتيح لك النطاق المحدود تقديم الوظائف حيثما تكون مطلوبة وتجنُّب استخدامها عن طريق الخطأ في الحالات التي لا تكون فيها مناسبة.

إليك مثال على ذلك في ميزة "الإنشاء". عند استدعاء Row layout composable، يتم استدعاء دالة 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(
            /*...*/
            /* ...
        )
    }
)

لمزيد من المعلومات، اطّلِع على الدوالّ الثابتة التي تستخدم ملف برمجيًا 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

إزالة بنية فئات البيانات

في حال تحديد class data، يمكنك الوصول إلى البيانات بسهولة باستخدام بيان إزالة التنظيم. على سبيل المثال، لنفترض أنّك حدّدت فئة Person:

data class Person(val name: String, val age: Int)

إذا كان لديك عنصر من هذا النوع، يمكنك الوصول إلى قيمه باستخدام رمز مثل هذا:

val mary = Person(name = "Mary", age = 35)

// ...

val (name, age) = mary

سيظهر لك هذا النوع من الرموز غالبًا في دوال الإنشاء:

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. غالبًا ما تستخدم ميزة "الإنشاء" هذه العناصر. على سبيل المثال، يتم تعريف MaterialTheme على أنّه كائن مفردتون، وتحتوي كل السمات MaterialTheme.colors وshapes وtypography على قيم للمظهر الحالي.

أدوات الإنشاء ولغات برمجة التطبيقات المحدودة النطاق الآمنة من حيث النوع

تسمح 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 composable، فإنّه يأخذ كمَعلمة دالة مع 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)
        }
    }
}

يمكنك الاطّلاع على المزيد من المعلومات حول أدوات الإنشاء وDSLS الآمنة في النوع في مستندات Kotlin.

كوروتينات Kotlin

توفّر الدوالّ المتزامنة دعمًا للبرمجة غير المتزامنة على مستوى اللغة في Kotlin. يمكن للوظائف المتكررة تعليق التنفيذ بدون حظر سلاسل المهام. إنّ واجهة مستخدم الاستجابة العالية تكون غير متزامنة بطبيعتها، ويحلّ Jetpack Compose هذه المشكلة من خلال استخدام وحدات معالجة المهام المتعدّدة في وقت واحد على مستوى واجهة برمجة التطبيقات بدلاً من استخدام وظائف الاستدعاء.

يوفّر Jetpack Compose واجهات برمجة تطبيقات تجعل استخدام الدوالّ المتعدّدة المهام آمنًا ضمن طبقة واجهة المستخدم. تعرض الدالة rememberCoroutineScope السمة CoroutineScope التي يمكنك من خلالها إنشاء الكوروتين في معالِجات الأحداث واستدعاء واجهات برمجة تطبيقات التعليق المعلَّق. يمكنك الاطّلاع على المثال أدناه باستخدام واجهة برمجة تطبيقات 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()
        }
    }
) { /* ... */ }

تنفِّذ الدوالّ المتعدّدة المهام مجموعة الرموز البرمجية بشكل تسلسلي تلقائيًا. إنّ دالّة ملفّ برمجي متعدّد المهام التي تعمل توقّف تنفيذها إلى أن تُرجع دالّة suspend. وينطبق ذلك حتى إذا كانت دالة التعليق تنقل عملية التنفيذ إلى علامة 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.