Mengapa kami memilih Kotlin sebagai salah satu bahasa target kami. Bagian 2: Kotlin Multiplatform

Kami melanjutkan serangkaian artikel tentang memperkenalkan bahasa Kotlin ke dalam proses pengembangan kami. Cari bagian pertama di sini .

Pada 2017, proyek ambisius dari Jetbrains menyaksikan hari yang cerah, menawarkan perspektif baru tentang pengembangan lintas platform. Kompilasi kode kotlin menjadi kode asli berbagai platform! Kami di Domklik, pada gilirannya, selalu mencari cara untuk mengoptimalkan proses pengembangan. Apa yang bisa lebih baik daripada menggunakan kembali kode, kami pikir? Itu benar - jangan menulis kode sama sekali. Dan agar semuanya berfungsi seperti yang Anda inginkan. Tetapi sementara ini tidak terjadi. Dan jika ada solusi yang memungkinkan kita, tanpa menghabiskan banyak usaha, untuk menggunakan basis kode tunggal untuk platform yang berbeda, mengapa tidak mencoba?

Halo semuanya! Nama saya Gennady Vasilkov, saya seorang pengembang android di Domklik dan hari ini saya ingin berbagi dengan Anda pengalaman kami dalam mengembangkan Kotlin Multiplatform untuk perangkat seluler, beri tahu kami kesulitan apa yang kami hadapi, bagaimana kami menyelesaikan dan apa yang akhirnya kami lakukan. Topiknya pasti akan menarik bagi mereka yang ingin mencoba Kotlin MPP (proyek Multiplatform), atau sudah mencobanya, tetapi tidak menyelesaikan produksi. Atau membawa, tetapi tidak seperti yang saya inginkan. Saya akan mencoba menyampaikan visi kami tentang bagaimana proses pengembangan dan pengiriman perpustakaan yang dikembangkan harus diatur (menggunakan salah satunya sebagai contoh, saya akan memberi tahu Anda awal dari jalur pengembangan kami di Kotlin MPP).

Anda ingin cerita bagaimana kami melakukannya? Kami memilikinya!



Secara singkat tentang teknologi


Bagi mereka yang belum mendengar atau tidak terjun ke topik, itu menggantung di dunia Jawa dan hanya pergi ke dunia Kotlin (atau tidak pergi, tetapi mengintip): Kotlin MPP adalah teknologi yang memungkinkan Anda untuk menggunakan kode yang pernah ditulis pada banyak platform sekaligus.

Perkembangannya, tidak mengherankan, dalam bahasa Kotlin di IntelliJ IDEA atau Android Studio. Nilai tambahnya adalah semua pengembang android (setidaknya bersama kami) tahu dan menyukai bahasa dan lingkungan pengembangan yang luar biasa ini.

Juga merupakan nilai tambah besar dalam mengkompilasi kode yang dihasilkan ke bahasa-bahasa asli untuk setiap platform (OBJ-C, JS, semuanya jelas dengan JVM).

Yaitu, semuanya keren. Menurut pendapat saya, Kotlin Multiplatform ideal untuk membawa logika bisnis ke perpustakaan. Dimungkinkan juga untuk menulis aplikasi sepenuhnya, tetapi untuk saat ini terlihat terlalu ekstrem, tetapi pengembangan perpustakaan cocok untuk proyek-proyek besar seperti kita.

Sedikit teknis, untuk memahami bagaimana proyek ini bekerja di MPP Kotlin


  • Sistem build adalah Gradle , mendukung sintaks dalam skrip groovy dan kotlin (kts).
  • Kotlin MPP memiliki konsep target - platform target. Dalam blok ini, sistem operasi yang kita butuhkan dikonfigurasi, kode asli yang akan mengkompilasi kode kita di Kotlin. Pada gambar di bawah ini 2 target diimplementasikan, jvm dan js (gambar sebagian diambil dari situs web ):

    Dukungan untuk platform lain diimplementasikan dengan cara yang sama.
  • Set sumber - nama itu sendiri jelas bahwa kode sumber untuk platform disimpan di sini. Ada set sumber umum dan platform (ada banyak dari mereka karena ada target dalam proyek ini, ini diikuti oleh IDE). Tidak mungkin untuk tidak menyebutkan mekanisme harapan-aktual di sini.



    Mekanisme ini memungkinkan pengaksesan kode khusus platform dari modul Common. Kami mendeklarasikan pernyataan harapan dalam modul Umum dan mengimplementasikannya di platform. Di bawah ini adalah contoh penggunaan mekanisme untuk mendapatkan tanggal pada perangkat:

    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()
    

    Seperti yang Anda lihat untuk modul platform, pustaka sistem Java dan iOS Foundation masing-masing tersedia.

Poin-poin ini cukup untuk memahami apa yang akan dibahas nanti.

Pengalaman kami


Jadi, pada titik tertentu, kami memutuskan bahwa semuanya diterima oleh Kotlin MPP (kemudian juga disebut Kotlin / Asli) sebagai standar dan kami mulai menulis perpustakaan tempat kami berbagi kode umum. Pada awalnya itu hanya kode untuk platform mobile, di beberapa titik mereka menambahkan dukungan untuk jvm backend. Di android, pengembangan dan publikasi perpustakaan yang dikembangkan tidak menimbulkan masalah, pada iOS, dalam praktiknya, mereka menghadapi beberapa masalah, tetapi mereka berhasil diselesaikan dan model kerja untuk mengembangkan dan menerbitkan kerangka kerja berhasil.

Saatnya sesuatu untuk nongkrong!


Kami telah melakukan berbagai fungsi di perpustakaan terpisah yang kami gunakan dalam proyek utama.

1) Analisis


Latar belakang kejadian


Bukan rahasia lagi bahwa semua aplikasi seluler mengumpulkan banyak analitik. Acara digantung dengan semua peristiwa penting dan tidak terlalu (misalnya, dalam aplikasi kita lebih dari 600 berbagai metrik dikumpulkan). Dan apa itu koleksi metrik? Secara sederhana, ini adalah panggilan ke fungsi yang mengirimkan acara dengan kunci tertentu ke isi mesin analitik. Dan kemudian masuk ke berbagai sistem analitik seperti firebase, appmetrica, dan lainnya. Apa masalah dengan ini? Duplikasi konstan dari kode yang sama (plus atau minus) pada dua platform! Ya, dan faktor manusia tidak dapat ditemukan di mana pun - pengembang bisa keliru, baik atas nama kunci dan dalam set meta-data yang dikirimkan dengan acara tersebut. Ini jelas perlu ditulis sekali dan digunakan pada setiap platform. Kandidat ideal untuk membuat logika umum dan teknologi pengujian (ini adalah pena uji kami di Kotlin Mpp).

Bagaimana cara mengimplementasikannya


Kami mentransfer peristiwa itu sendiri ke perpustakaan (dalam bentuk fungsi dengan kunci kabel dan satu set data meta yang diperlukan dalam argumen) dan menulis logika baru untuk memproses peristiwa ini. Format acara disatukan dan mesin pawang (bus) dibuat, di mana semua acara dituangkan. Dan untuk berbagai sistem analitik, pendengar adaptor menulis (solusi yang sangat berguna ternyata, ketika menambahkan analitik baru, kita dapat dengan mudah mengarahkan ulang semuanya, atau secara selektif, setiap peristiwa ke analitik baru).



Menarik dalam proses implementasi


Karena fitur penerapan bekerja dengan stream di Kotlin / Native untuk iOS, Anda tidak bisa hanya mengambil dan bekerja dengan objek Kotlin sebagai singleton dan menulis data di sana kapan saja. Semua objek yang mentransfer statusnya di antara utas harus dibekukan (ada fungsi beku () untuk ini). Tetapi perpustakaan, antara lain, menyimpan keadaan yang dapat berubah selama kehidupan aplikasi. Ada beberapa cara untuk mengatasi situasi ini, kami memutuskan yang paling sederhana:

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>

Opsi ini cocok untuk penyimpanan keadaan sederhana. Dalam kasus kami, konfigurasi platform disimpan:

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)
}

Mengapa ini dibutuhkan? Kami tidak mendapatkan beberapa informasi di awal aplikasi, ketika modul seharusnya sudah dimuat dan objeknya sudah dalam keadaan beku. Untuk menambahkan informasi ini ke konfigurasi dan kemudian dapat dibaca dari aliran yang berbeda di iOS dan langkah seperti itu diperlukan.

Kami juga menghadapi masalah penerbitan perpustakaan. Dan jika Android tidak memiliki masalah dengan ini, maka metode yang diusulkan oleh dokumentasi resmi pada waktu itu untuk menghubungkan kerangka kerja yang dirakit secara langsung ke proyek iOS tidak sesuai dengan kami karena sejumlah alasan, saya ingin mendapatkan versi baru yang sesederhana dan setransparan mungkin. Plus, versi didukung.
Solusinya adalah dengan menghubungkan kerangka kerja melalui file pod. Untuk melakukan ini, mereka membuat file podspec dan kerangka itu sendiri, menempatkannya di repositori dan menghubungkan pustaka ke proyek menjadi sangat sederhana dan nyaman bagi pengembang iOS. Juga, sekali lagi, untuk kenyamanan pengembangan, kami mengumpulkan artefak tunggal untuk semua arsitektur, kerangka kerja yang disebut lemak (sebenarnya biner tebal di mana semua arsitektur dan menambahkan file meta yang dihasilkan oleh plugin kotlin bergaul bersama). Implementasi semua ini di bawah spoiler:

Siapa yang peduli bagaimana kami melakukannya sebelum keputusan resmi
Setelah plugin kotlin mengumpulkan banyak kerangka kerja berbeda untuk arsitektur berbeda, kami secara manual membuat kerangka universal baru tempat kami menyalin data meta dari apa pun (dalam kasus kami, kami mengambil 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!


Pada saat penulisan, ada solusi resmi untuk membuat kerangka kerja yang gemuk dan menghasilkan file podspec (dan umumnya integrasi dengan CocoaPods). Jadi kita hanya perlu mengunggah kerangka kerja dan file podspec ke repositori dan menghubungkan proyek di iOS dengan cara yang sama seperti sebelumnya:




Setelah menerapkan skema tersebut dengan satu tempat untuk menyimpan acara, mekanisme untuk memproses dan mengirim metrik, proses kami menambahkan metrik baru dikurangi menjadi fakta bahwa satu pengembang menambahkan fungsi baru ke perpustakaan, yang menggambarkan kunci acara dan meta-data yang diperlukan yang harus ditransmisikan oleh pengembang untuk acara tertentu . Lebih jauh, pengembang platform lain hanya mengunduh versi baru perpustakaan dan menggunakan fungsi baru di tempat yang tepat. Telah menjadi jauh lebih mudah untuk mempertahankan konsistensi seluruh sistem analitik, kini juga jauh lebih mudah untuk mengubah implementasi internal bus peristiwa. Sebagai contoh, di beberapa titik, untuk salah satu analis kami, perlu bagi kami untuk menerapkan buffer acara kami sendiri, yang dilakukan segera di perpustakaan, tanpa perlu mengubah kode pada platform. Keuntungan!

Kami terus mentransfer kode semakin banyak ke Kotlin MPP, dalam artikel berikut saya akan melanjutkan kisah pengantar kami ke dunia pengembangan lintas platform menggunakan dua perpustakaan lebih sebagai contoh, di mana serialisasi dan database digunakan, bekerja dengan waktu. Saya akan memberi tahu Anda tentang batasan yang ditemui saat menggunakan beberapa kerangka kerja kotlin dalam satu proyek iOS dan cara mengatasi batasan ini.

Dan sekarang secara singkat tentang hasil penelitian kami


+ Semuanya bekerja dengan stabil dalam produksi.
+ Basis kode terpadu logika layanan bisnis untuk semua platform (lebih sedikit kesalahan dan perbedaan).
+ Mengurangi biaya dukungan dan mengurangi waktu pengembangan untuk persyaratan baru.
+ Kami meningkatkan keahlian pengembang.

tautan yang berhubungan


Situs web Multiplin Platform Kotlin: www.jetbrains.com/lp/mobilecrossplatform

ZY:
Saya tidak ingin mengulangi tentang bahasa singkat, dan karenanya fokus pada logika bisnis saat menulis kode. Saya dapat menyarankan beberapa artikel yang

membahas topik ini: “Kotlin di bawah tenda - lihat dekode yang didekompilasi” - sebuah artikel yang menjelaskan pengoperasian kompiler Kotlin dan membahas struktur dasar bahasa.
habr.com/en/post/425077
Dua bagian artikel "Kotlin, kompilasi dalam bytecode dan kinerja", melengkapi artikel sebelumnya.
habr.com/ru/company/inforion/blog/330060
habr.com/ru/company/inforion/blog/330064
Pasha Finkelstein melaporkan "Kotlin - dua tahun dalam produksi dan bukan celah tunggal", yang didasarkan pada pengalaman menggunakan dan mengimplementasikan Kotlin di perusahaan kami.
www.youtube.com/watch?v=nCDWb7O1ZW4

All Articles