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

متغيّرات lambda اللاحقة

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

على سبيل المثال، المعلَمة الأخيرة لجميع التنسيقات، مثل الدالة القابلة للتجميع Column() ، هي content، وهي دالة تُنشئ عناصر واجهة مستخدم العنصر الفرعي. لنفترض أنّك أردت إنشاء عمود يحتوي على ثلاثة عناصر نصية، وعليك تطبيق بعض التنسيقات. سيعمل هذا الرمز، ولكنه معقد جدًا:

Column(
    modifier = Modifier.padding(16.dp),
    content = {
        Text("Some text")
        Text("Some more text")
        Text("Last text")
    }
)

بما أنّ المَعلمة content هي الأخيرة في توقيع الدالة، ونحن نُمرِّر قيمتها كتعبير لامبادا، يمكننا إخراجها من بين المربّعات:

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

لمزيد من المعلومات، اطّلِع على الدوالّ الثابتة مع المستلِم في مستندات 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 لغات وصفية لبعض واجهات برمجة التطبيقات، مثل 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)
        }
    }
}

اطّلِع على مزيد من المعلومات عن أدوات الإنشاء الآمنة من النوع ولغات وصف البيانات في مستندات Kotlin.

كوروتينات Kotlin

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

يوفّر Jetpack Compose واجهات برمجة تطبيقات تجعل استخدام الدوالّ المتعدّدة المهام آمنًا ضمن طبقة واجهة المستخدم. تعرض الدالة rememberCoroutineScope قيمة CoroutineScope يمكنك من خلالها إنشاء وظائف معالجة متزامنة في معالجات الأحداث وطلب Compose suspend APIs. اطّلِع على المثال أدناه باستخدام واجهة برمجة التطبيقات 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
                        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.