المشاريع متعددة المنصات (Multiplatform) في لغة Kotlin

من موسوعة حسوب

ملاحظة: ما تزال ميّزة تعدّد منصّات العمل تجريبيّة في الإصدار Kotlin 1.2 وبالتالي فإن ما تحتويه هذه الصفحة قد يخضع للتغيير في إصدارات Kotlin القادمة.

يسمح مشروع Kotlin متعدّد المنصّات بترجمة الشيفرة ذاتها إلى عدّة منصّاتٍ للعمل (platforms)، وتدعم Kotlin حاليًا البيئات: JVM و JavaScript بالإضافة إلى Native والتي لا زلت قيد التطوير وستُضاف رسميًا فيما بعد.

بُنية المشروع متعدّد المنصّات (Multiplatform Project Structure)

يتألف من ثلاثة أنواع من الوحدات (modules):

  • الوحدة المشتركة (common module): تحتوي على الشيفرة العامّة التي لا تتبع لمنصّة عمل مُحدَّدة، بالإضافة إلى التصريحات التي ليس لها تعريف استخدامٍ (implementations) للواجهات API بحسب المنصّة، إذ تسمح هذه التصريحات للشيفرة المشتركة بالاعتماد على تعريفات الاستخدام الخاصّة بالمنصّة.
  • وحدة المنصّة ( platform module): تتضمن تعاريف الاستخدام (implementations) للتصريحات الواردة في الوحدة المشتركة وذلك بحسب منصّة العمل الحاليّة، بالإضافة إلى الشيفرة التي تتناسب مع هذه المنصّة، وتكون وحدة المنصّة دائمًا بمثابة تعريف الاستخدام لواحدةٍ -فقط- من الوحدات المشتركة.
  • الوحدة النظاميّة (regular module): تستهدف منصّةً معيّنةً وإما أن تكون تابعةً (dependency) لوحدات المنصّة أو أن تعتمد عليها.

ويمكن أن تعتمد الوحدة المشتركة على وحداتٍ مشتركةٍ أخرى وبعض المكتبات فقط، متضمنةً الإصدار المشترك من المكتبة القياسيّة في Kotlin‏ (kotlin-stdlib-common)، وتحتوي على شيفرة Kotlin فقط ولا يُسمح بوجود شيفرة أيّ لغةٍ أخرى ضمنها.

أمّا وحدة المنصّة فهي تعتمد على أيّ وحدةٍ أخرى بالإضافة إلى المكتبات المُتاحة في المنصّة المستهدفة (أي مكتبات Java عند العمل في بيئة Kotlin/JVM ومكتبات JS عند العمل في بيئة Kotlin/JS)، إذ تشمل بيئة Kotlin/JVM أيّ لغة برمجةٍ متوافقةٍ مع JVM ولا تقتصر على لغة Java وحسب.

سينتُج عن ترجمة (compiling) الشيفرة الموجودة في الوحدة المشتركة ملفٌ بمعلوماتٍ توصيفيّةٍ (metadata) تشمل كافّة التصريحات الموجودة في الوحدة، أمّا ترجمة وحدة المنصّة فستنتِج شيفرةً خاصّة بالمنصّة المستهدفة (أي bytecode في حالة JVM وشيفرة مصدرية [source code] في حالة JavaScript)، وتكون هذه الشيفرة ناتجةً عن الشيفرة في وحدة المنصّة والوحدة المشتركة التي يُعرَّف استخدامها (implemented).

وبالتالي فإنّ كلّ مكتبةٍ متعدّدة المنصّات يجب تكون موزّعةً على شكل مجموعاتٍ من الأدوات، وهي عبارة عن ملف ‎.jar مشتركٍ يحتوي على البيانات التوصيفيّة للشيفرة المشتركة، بالإضافة إلى ملفات ‎.jar (مخصَّصة المنصّة) والتي تحتوي على تعريف الاستخدام المُترجَم (compiled implementation) لكلِّ منصّة.

إعداد مشروعٍ متعدّد المنصّات (Setting Up a Multiplatform Project)

يجب إعداد المشروع باستخدام نظام Gradle في الإصدار Kotlin 1.2 إذ لا يتوفر الدعم لأيّ نظام بناءٍ آخر غيره، فلإنشاء مشروعٍ جديدٍ يجب تحديد الخيار "Kotlin (Multiplatform)‎" الموجود تحت مسمّى "Kotlin" في مربع الحوار "New Project" (مشروع جديد)، وسيُنشَأ مشروعٌ جديدٌ يحتوي على ثلاث وحدات (modules): الوحدة المشتركة ووحدتين لبيئتيّ JVM و JS، ولإضافة المزيد من الوحدات اختر أحد الخيارات من "Kotlin (Multiplatform)‎" الموجودة تحت المُسمّى Gradle في مربع الحوار "New Module" (وحدة جديدة).

أمّا عند الحاجة لضبط إعدادت المشروع يدويًا فيجب القيام بالخطوات الآتية:

  1. أضف الإضافة (plugin)‏ Kotlin Gradle إلى مسار صنف نصّ البناء (buildscript classpath):‏ classpath‎ "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version".
  2. - طبِّق الإضافة kotlin-platform-common على الوحدة المشتركة.
  3. - طبق الإضافات kotlin-platform-jvm و kotlin-platform-android و kotlin-platform-js على وحدات المنصّة لكلِّ من  JVM و Android و JS على الترتيب.
  4. - أضف بعض الاعتماديّات (dependencies) بالمجال expectedBy من وحدات المنصّة إلى الوحدة المشتركة.

ويوضح المثال الآتي ملفًا كاملًا (build.gradle) للوحدة المشتركة بالإصدار Kotlin 1.2-Beta :

buildscript {
    ext.kotlin_version = '1.2.30'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin-platform-common'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version"
}

كما يوضِّح المثال الآتي ملفًا كاملًا build.gradle لوحدة منصّة JVM (لاحظ السطر expectedBy الموجود في بُنية dependencies):

buildscript {
    ext.kotlin_version = '1.2.30'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin-platform-jvm'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    expectedBy project(":")
    testCompile "junit:junit:4.12"
    testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}

التصريحات المُخصَّصة لمنصَّة العمل (Platform-Specific Declarations)

إنّ من إحدى المميّزات القويّة في Kotlin هو أنّ الشيفرة متعدّدة المنصّات تسمح للشيفرة المشتركة بالاعتماد على تصريحاتٍ مُخصَّصةٍ للمنصّة، وسيتطلَّب هذا في لغات البرمجة الأخرى بناء مجموعةٍ من الواجهات في الشيفرة المشتركة وتعريف استخدامها (implementing) في وحدات المنصّة، ولكن بالمقابل لا تكون هذه الطريقة مثاليةً في بعض الحالات عند وجود مكتبةٍ في إحدى المنصّات التي تعرِّف استخدام الوظائف (functionality) المطلوبة، ومن الأفضل استخدام الواجهة (API) من هذه المكتبة مباشرةً بدون أي مغلِّفات إضافيّة (extra wrappers)، كما يجب أيضًا التعبير عن التصريحات المشتركة كواجهاتٍ (interfaces) لا تشمل كافّة الحالات الممكنة.

توفِّر Kotlin -كبديلٍ عن ذلك- آليّة التصريحات المتوقَّعة (expected) والفعليّة (actual) حيث تعرِّف الوحدة المشتركة بعض التصريحات المتوقَّعة، وتعرِّف وحدة المنصّة التصريحات الفعليّة للتصريحات المتوقّعة في الوحدة المشتركة، ويتَّضح ذلك عبر المثال الآتي (وهو جزءٌ من الوحدة المشتركة):

package org.jetbrains.foo

expect class Foo(bar: String) {
    fun frob()
}

fun main(args: Array<String>) {
    Foo("Hello").frob()
}

وهذه شيفرة الوحدة الموافقة للشيفرة السابقة في JVM:

package org.jetbrains.foo

actual class Foo actual constructor(val bar: String) {
    actual fun frob() {
        println("Frobbing the $bar")
    }
}

وتتلخّص الآلية بالنقاط المهمّة الآتية:

  • يكون للتصريح المُتوقَّع (في الوحدة المشتركة) والتصريح الفعليّ الموافق له (في وحدة المنصّة) نفس الاسم تمامًا.
  • يُحدَّد التصريح المُتوقَّع بالكلمة المفتاحيّة expect والفعليّ بالكلمة المفتاحية actual.
  • يجب أن تُحدَّد كل التصريحات الفعليّة التي تطابق (match) أيًا من التصريحات المُتوقَّعة بالكلمة المفتاحيّة actual.
  • لا تحتوي التصريحات المُتوقَّعة على أيّ تعريفٍ للاستخدام (implementation).

ولا تقتصر التصريحات المُتوقَّعة على الواجهات (interfaces) وعناصرها فقط، ففي المثال الآتي يوجد للصنف المُتوقَّع بانٍ (constructor) ويمكن إنشاؤه مباشرةً من الشيفرة المشتركة، كما ويمكن تطبيق المُحدِّد expect على التصريحات الأخرى بما فيها تصريحات المستوى الأعلى (top-level) والتوصيفات (annotations)، مثل:

// المشتركة
expect fun formatString(source: String, vararg args: Any): String

expect annotation class Test

// JVM
actual fun formatString(source: String, vararg args: Any) =
    String.format(source, args)
    
actual typealias Test = org.junit.Test

ويتأكَّد المُترجِم من وجود تصريحٍ فعليّ لكل تصريحٍ مُتوقَّعٍ، وذلك في كلِّ وحدات المنصّات التي تعرَّف استخدام (implement) الوحدة المشتركة، وسيُعلِم بوجود خطأٍ عند عدم وجود كلِّ التصريحات الفعليّة المطلوبة، وتساعد بيئة العمل (IDE) بإنشاء غير الموجود منها. عند الحاجة لاستخدام مكتبةٍ خاصّةٍ بالمنصّة في الوحدة المشتركة بينما يتوفَّر تعريف الاستخدام (implementation) لمنصّة أخرى، فيمكن الاعتماد حينئذٍ على التسمية البديلة للنوع (typealias) للصنف الموجود باعتباره التصريحَ الفعليّ، كما في الشيفرة:

expect class AtomicRef<V>(value: V) {
  fun get(): V
  fun set(value: V)
  fun getAndSet(value: V): V
  fun compareAndSet(expect: V, update: V): Boolean
}

actual typealias AtomicRef<V> = java.util.concurrent.atomic.AtomicReference<V>

اختبارات المنصّات المتعدِّدة (Multiplatform Tests)

تتيح لغة Kotlin إمكانيّة كتابة الاختبارات في المشروع المشترك بحيث تُترجَم وتُنفَّذ في كلّ مشروعٍ خاصٍ بالمنصّة، وهناك أربعة توصيفاتٍ (annotations) مُتاحةٍ في الحزمة kotlin.test والمستخدَمة لترميز (markup) الاختبارات في الشيفرة المشتركة وهي: ‎@Test و ‎@Ignore و ‎‎@BeforeTest و ‎@AfterTest وهي متوافقةٌ مع توصيفات JUnit الأربعة الموجودة في JVM ، وهي متاحة في JS بدءًا من الإصدار 1.1.4 وذلك لدعم اختبارات وحدات JS.

ولاستخدامها تجب إضافة الاعتماديّة (dependency) على kotlin-test-annotations-common إلى الوحدة المشتركة، والاعتماديّة على kotlin-test-junit إلى وحدة JVM، والاعتماديّة على kotlin-test-js إلى وحدة JS.

مصادر