لماذا اخترنا Kotlin كإحدى اللغات المستهدفة لدينا. الجزء 2: Kotlin Multiplatform

نواصل سلسلة المقالات حول إدخال لغة Kotlin في عملية التطوير لدينا. ابحث عن الجزء الأول هنا .

في عام 2017 ، شهد المشروع الطموح من Jetbrains النور ، حيث يقدم منظورًا جديدًا للتطوير عبر الأنظمة الأساسية. تجميع كود kotlin في كود أصلي لمنصات مختلفة! نحن في Domklik ، بدورنا ، نبحث دائمًا عن طرق لتحسين عملية التطوير. اعتقدنا ما يمكن أن يكون أفضل من إعادة استخدام التعليمات البرمجية؟ هذا صحيح - لا تكتب الرمز على الإطلاق. بحيث يعمل كل شيء كما تريد. ولكن في حين أن هذا لا يحدث. وإذا كان هناك حل يسمح لنا ، دون بذل الكثير من الجهد ، باستخدام قاعدة رمز واحدة لمنصات مختلفة ، فلماذا لا تحاول؟

مرحبا للجميع! اسمي جينادي فاسيلكوف ، وأنا مطور أندرويد في Domklik ، وأريد اليوم أن أشارك معك خبرتنا في تطوير Kotlin Multiplatform للأجهزة المحمولة ، وأخبرنا بالصعوبات التي واجهناها ، وكيف تم حلها وما انتهى بنا. سيكون الموضوع بالتأكيد موضع اهتمام لأولئك الذين يرغبون في تجربة Kotlin MPP (مشاريع متعددة المنصات) ، أو جربوها بالفعل ، لكنهم لم ينتهوا من الإنتاج. أو أحضر ، ولكن ليس كما أريد. سأحاول أن أنقل رؤيتنا لكيفية ترتيب عملية تطوير المكتبات المتطورة وتقديمها (باستخدام واحدة منها كمثال ، سأخبرك ببداية مسار التطوير في Kotlin MPP).

هل تريد قصص كيف فعلناها؟ لدينا منهم!



باختصار حول التكنولوجيا


بالنسبة لأولئك الذين لم يسمعوا بعد أو لم يغرقوا في الموضوع ، فإنهم عالقون في عالم جافا ولا يذهبون إلا إلى عالم Kotlin (أو لا يذهبوا سوى زقزقة): Kotlin MPP هي تقنية تسمح لك باستخدام رمز مكتوب مرة واحدة على العديد من المنصات في وقت واحد.

ليس من المستغرب أن يكون التطوير بلغة Kotlin في IntelliJ IDEA أو Android Studio. زائد هو أن جميع مطوري Android (على الأقل معنا) يعرفون ويحبون كل من اللغة وبيئات التطوير الرائعة هذه.

أيضًا إضافة كبيرة في تجميع الكود الناتج إلى لغات أصلية لكل منصة (OBJ-C ، JS ، كل شيء واضح مع JVM).

أي أن كل شيء رائع. في رأيي ، تعتبر Kotlin Multiplatform مثالية لأخذ منطق الأعمال إلى المكتبات. من الممكن أيضًا كتابة طلب كامل تمامًا ، ولكن في الوقت الحالي يبدو متطرفًا للغاية ، ولكن تطوير المكتبة مناسب لمشاريع كبيرة مثل مشروعنا.

القليل من التقنية ، لفهم كيفية عمل المشروع على Kotlin MPP


  • نظام البناء هو Gradle ، وهو يدعم بناء الجملة في نص رائع و kotlin (kts).
  • لدى Kotlin MPP مفهوم الأهداف - المنصات المستهدفة. في هذه الكتل ، يتم تكوين أنظمة التشغيل التي نحتاجها ، والتي سيقوم كودها الأصلي بتجميع التعليمات البرمجية الخاصة بنا على Kotlin. في الصورة أدناه يتم تنفيذ هدفين ، jvm و js (يتم التقاط الصورة جزئيًا من موقع الويب ):

    يتم تنفيذ دعم المنصات الأخرى بنفس الطريقة.
  • مجموعات المصادر - الاسم نفسه واضح أن رموز المصدر للأنظمة الأساسية مخزنة هنا. هناك مجموعة مصادر ومنصة مشتركة (يوجد العديد منها في أهداف المشروع ، ويتبعها IDE). من المستحيل عدم ذكر آلية التوقعات الفعلية هنا.



    تسمح الآلية بالوصول إلى التعليمات البرمجية الخاصة بالنظام الأساسي من الوحدة النمطية المشتركة. نعلن عن إعلان متوقع في الوحدة المشتركة وننفذه في الأنظمة الأساسية. فيما يلي مثال على استخدام الآلية للحصول على التاريخ على الأجهزة:

    Common:
    internal expect val timestamp: Long
    
    Android/JVM:
    internal actual val timestamp: Long 
        get() = java.lang.System.currentTimeMillis()
    
    iOS:
    internal actual val timestamp: Long 
        get() = platform.Foundation.NSDate().timeIntervalSince1970.toLong()
    

    كما ترى في وحدات النظام الأساسي ، تتوفر مكتبات نظام Java و iOS Foundation ، على التوالي.

هذه النقاط كافية لفهم ما سيتم مناقشته لاحقًا.

تجربتنا


لذلك ، في مرحلة ما ، قررنا أن كل شيء تم قبوله من قبل Kotlin MPP (ثم كان يطلق عليه أيضًا Kotlin / Native) كمعيار وبدأنا في كتابة المكتبات التي أخذنا فيها الكود المشترك. في البداية كان الرمز فقط لمنصات الجوال ، في مرحلة ما أضافوا دعمًا لـ jvm backend. على نظام Android ، لم يتسبب تطوير المكتبات المتطورة ونشرها في حدوث مشكلات ، على نظام التشغيل iOS ، في الواقع ، واجهوا بعض المشكلات ، ولكن تم حلها بنجاح وتم تطوير نموذج عمل لتطوير ونشر الأطر.

حان الوقت لشيء ما للتسكع!


قمنا بتنفيذ العديد من الوظائف في مكتبات منفصلة نستخدمها في المشروع الرئيسي.

1) التحليلات


خلفية الحدوث


ليس سراً أن جميع تطبيقات الجوال تجمع مجموعة من التحليلات. يتم تعليق الأحداث مع جميع الأحداث المهمة وليس ذاتها (على سبيل المثال ، في تطبيقنا يتم جمع أكثر من 600 مقياس مختلف). وما هي المجموعة المترية؟ بطريقة بسيطة ، هذه دعوة لوظيفة ترسل حدثًا بمفتاح معين لأمعاء محرك التحليلات. ثم يذهب إلى مجموعة متنوعة من أنظمة التحليلات مثل Firebase و appmetrica وغيرها. ما هي المشاكل الموجودة في هذا؟ الازدواج المستمر لنفس الكود (زائد أو ناقص) على منصتين! نعم ، ولا يمكن العثور على العامل البشري في أي مكان - يمكن أن يخطئ المطورون ، سواء في اسم المفاتيح أو في مجموعة البيانات الوصفية المرسلة مع الحدث. من الواضح أن هذا يحتاج إلى كتابته مرة واحدة واستخدامه على كل منصة. مرشح مثالي لصنع تكنولوجيا المنطق والاختبار العامة (هذا هو قلم الاختبار الخاص بنا في Kotlin Mpp).

كيفية التنفيذ


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



مثيرة للاهتمام في عملية التنفيذ


نظرًا لميزات تنفيذ العمل مع التدفقات في Kotlin / Native for iOS ، لا يمكنك فقط التعامل مع كائن Kotlin والعمل معه كلغة فردية وكتابة البيانات هناك في أي وقت. يجب تجميد جميع الكائنات التي تنقل حالتها بين الخيوط (هناك وظيفة تجميد () لهذا). لكن المكتبة ، من بين أمور أخرى ، تخزن حالة يمكن أن تتغير أثناء حياة التطبيق. هناك عدة طرق لحل هذا الموقف ، استقرنا على أبسطها:

Common:

expect class AtomicReference<T>(value_: T) {
    var value: T
}

Android:

actual class AtomicReference<T> actual constructor(value_: T) {
    actual var value: T = value_
}

iOS:

actual typealias AtomicReference<V> = kotlin.native.concurrent.AtomicReference<V>

هذا الخيار مناسب لتخزين الحالة البسيطة. في حالتنا ، يتم تخزين تكوين النظام الأساسي:

object Config {
    val platform = AtomicReference<String?>("")
    var manufacturer = AtomicReference<String?>("")
    var model = AtomicReference<String?>("")
    var deviceId = AtomicReference<String?>("")
    var appVersion = AtomicReference<String?>("")
    var sessionId = AtomicReference<String?>("")
    var debug = AtomicReference<Boolean>(false)
}

لماذا هذا مطلوب؟ لا نحصل على بعض المعلومات في بداية التطبيق ، عندما يجب تحميل الوحدة بالفعل وتكون كائناتها بالفعل في حالة مجمدة. من أجل إضافة هذه المعلومات إلى التكوين ومن ثم يمكن قراءتها من تدفقات مختلفة على iOS ومثل هذه الخطوة مطلوبة.

كما واجهنا قضية نشر المكتبة. وإذا لم يكن لدى Android أي مشاكل في ذلك ، فإن الطريقة التي اقترحتها الوثائق الرسمية في ذلك الوقت لربط الإطار المجمّع مباشرة بمشروع iOS لم تناسبنا لعدد من الأسباب ، أردت الحصول على إصدارات جديدة بسيطة وشفافة قدر الإمكان. بالإضافة إلى ذلك ، يتم دعم الإصدارات.
الحل هو ربط إطار العمل عبر ملف جراب. للقيام بذلك ، قاموا بإنشاء ملف podspec والإطار نفسه ، ووضعهم في المستودع وأصبح ربط المكتبة بالمشروع بسيطًا للغاية وملائمًا لمطوري iOS. أيضًا ، مرة أخرى ، من أجل راحة التطوير ، نجمع قطعة أثرية واحدة لجميع الهياكل ، ما يسمى بإطار الدهون (في الواقع ثنائي جريء حيث تتلاقى جميع البنى والملفات الفوقية المضافة التي تم إنشاؤها بواسطة المكون الإضافي kotlin). تنفيذ هذا الأمر كله تحت المفسد:

من يهتم كيف فعلنا ذلك قبل القرار الرسمي
بعد أن جمع لنا مكون kotlin الإضافي مجموعة من الأطر المختلفة للهياكل المختلفة ، نقوم بإنشاء إطار عمل عالمي جديد يدويًا نقوم بنسخ بيانات التعريف من أي (في حالتنا ، أخذنا arm64):
//add meta files (headers, modules and plist) from arm64 platform
task copyMeta() {
    dependsOn build

    doLast {
        copy {
            from("$buildDir/bin/iphone/main/release/framework/${frameworkName}.framework")
            into "$buildDir/iphone_universal/${frameworkName}.framework"
        }
    }
}

, , , . lipo, :
//merge binary files into one
task lipo(type: Exec) {
    dependsOn copyMeta

    def frameworks = files(
            "$buildDir/bin/iphone32/main/release/framework/${frameworkName}.framework/$frameworkName",
            "$buildDir/bin/iphone/main/release/framework/${frameworkName}.framework/$frameworkName",
            "$buildDir/bin/iphoneSim/main/release/framework/${frameworkName}.framework/$frameworkName"
    )
    def output = file("$buildDir/iphone_universal/${frameworkName}.framework/$frameworkName")
    inputs.files frameworks
    outputs.file output
    executable = 'lipo'
    args = frameworks.files
    args += ['-create', '-output', output]
}

, - Kotlin MPP Info.plist UIRequiredDeviceCapabilities, ( ). PlistBuddy:
//workaround
//remove UIRequiredDeviceCapabilities key from plist file (because we copy this file from arm64, only arm64 architecture was available)
task editPlistFile(type: Exec) {
    dependsOn lipo

    executable = "/bin/sh"
    def script = './scripts/edit_plist_file.sh'
    def command = "Delete :UIRequiredDeviceCapabilities"
    def file = "$buildDir/iphone_universal/${frameworkName}.framework/Info.plist"

    args += [script, command, file]
}

edit_plist_file.sh:
/usr/libexec/PlistBuddy -c "$1" "$2"

, zip ( podspec ):
task zipIosFramework(type: Zip) {
    dependsOn editPlistFile
    from "$buildDir/iphone_universal/"
    include '**/*'
    archiveName = iosArtifactName
    destinationDir(file("$buildDir/iphone_universal_zipped/"))
}

podspec :
task generatePodspecFile(type: Exec) {
    dependsOn zipIosFramework

    executable = "/bin/sh"

    def script = './scripts/generate_podspec_file.sh'

    def templateStr = "version_name"
    def sourceFile = './podspec/template.podspec'
    def replaceStr = "$version"

    args += [script, templateStr, replaceStr, sourceFile, generatedPodspecFile]
}

generate_podscpec_file.sh :
sed -e s/"$1"/"$2"/g <"$3" >"$4"

curl :
task uploadIosFrameworkToNexus(type: Exec) {
    dependsOn generatePodspecFile

    executable = "/bin/sh"
    def body = "-s -k -v --user \'$userName:$password\' " +
            "--upload-file $buildDir/iphone_universal_zipped/$iosArtifactName $iosUrlRepoPath"
    args += ['-c', "curl $body"]
}

task uploadPodspecFileToNexus(type: Exec) {
    dependsOn uploadIosFrameworkToNexus

    executable = "/bin/sh"
    def body = "-s -k -v --user \'$userName:$password\' " +
            "--upload-file $generatedPodspecFile $iosUrlRepoPath"
    args += ['-c', "curl $body"]
}

:
task createAndUploadUniversalIosFramework() {
    dependsOn uploadPodspecFileToNexus
    doLast {
        println 'createAndUploadUniversalIosFramework complete'
    }
}


Profit!


في وقت كتابة هذا التقرير ، كانت هناك حلول رسمية لإنشاء إطار سمين وإنشاء ملف podspec (والتكامل بشكل عام مع CocoaPods). لذلك يتعين علينا فقط تحميل إطار العمل الذي تم إنشاؤه وملف podspec إلى المستودع وتوصيل المشروع في iOS بنفس الطريقة السابقة:




بعد تنفيذ مثل هذا المخطط بمكان واحد لتخزين الأحداث ، وآلية لمعالجة المقاييس وإرسالها ، تم تقليل عملية إضافة مقاييس جديدة إلى حقيقة أن مطورًا واحدًا يضيف وظيفة جديدة إلى المكتبة ، والتي تصف مفتاح الحدث والبيانات الوصفية الضرورية التي يجب على المطور إرسالها إلى حدث معين . علاوة على ذلك ، يقوم مطور نظام أساسي آخر بتنزيل إصدار جديد من المكتبة ويستخدم الوظيفة الجديدة في المكان الصحيح. لقد أصبح الحفاظ على تناسق نظام التحليلات بأكمله أسهل بكثير ؛ كما أصبح من السهل الآن تغيير التنفيذ الداخلي لناقل الأحداث. على سبيل المثال ، في مرحلة ما ، بالنسبة لأحد المحللين ، كان من الضروري بالنسبة لنا تنفيذ مخزن الأحداث الخاص بنا ، والذي تم على الفور في المكتبة ، دون الحاجة إلى تغيير الرمز على الأنظمة الأساسية. ربح!

نواصل نقل المزيد والمزيد من التعليمات البرمجية إلى Kotlin MPP ، في المقالات التالية ، سأستمر في قصة مقدمتنا إلى عالم التطوير عبر الأنظمة الأساسية باستخدام مكتبتين إضافيتين كأمثلة ، حيث تم استخدام التسلسل وقاعدة البيانات ، والعمل مع مرور الوقت. سأخبرك عن القيود التي تمت مواجهتها عند استخدام العديد من أطر عمل kotlin في مشروع iOS واحد وكيفية تجاوز هذا القيد.

والآن باختصار حول نتائج بحثنا


+ كل شيء يعمل بثبات في الإنتاج.
+ قاعدة كود موحدة لمنطق خدمة الأعمال لجميع المنصات (أخطاء وتناقض أقل).
+ تقليل تكاليف الدعم وتقليل وقت التطوير للمتطلبات الجديدة.
+ نزيد من خبرة المطورين.

روابط ذات علاقة


موقع Kotlin Multiplatform website: www.jetbrains.com/lp/mobilecrossplatform

ZY:
لا أريد أن أكرر عن مقتضبة اللغة ، وبالتالي أركز على منطق الأعمال عند كتابة التعليمات البرمجية. يمكنني أن أنصح بالعديد من المقالات التي

تغطي هذا الموضوع: "Kotlin تحت غطاء محرك السيارة - انظر فك الشفرة الباطلة" - مقال يصف عمل مترجم Kotlin ويغطي الهيكل الأساسي للغة.
habr.com/en/post/425077
جزءان من المقالة "Kotlin ، تجميع في البايت كود والأداء" ، مكملًا للمادة السابقة.
habr.com/ru/company/inforion/blog/330060
habr.com/ru/company/inforion/blog/330064
تقرير باشا فينكلشتاين "Kotlin - سنتان في الإنتاج وليس فجوة واحدة" ، والذي يستند إلى تجربة استخدام Kotlin وتنفيذها في شركتنا.
www.youtube.com/watch؟v=nCDWb7O1ZW4

All Articles