Diposting oleh Sergey Yeshin, Pengembang Android Middle Strong, DataArtLebih dari satu setengah tahun telah berlalu sejak Google mengumumkan dukungan resmi untuk Kotlin di Android, dan pengembang yang paling berpengalaman mulai bereksperimen dengan itu dalam pertempuran mereka dan tidak lebih dari tiga tahun yang lalu.
Bahasa baru diterima dengan hangat di komunitas Android, dan sebagian besar proyek Android baru akan dimulai dengan Kotlin. Juga penting bahwa Kotlin mengkompilasi menjadi bytecode JVM, oleh karena itu, sepenuhnya kompatibel dengan Java. Jadi, dalam proyek Android yang ada yang ditulis di Jawa, ada juga peluang (terlebih lagi, kebutuhan) untuk menggunakan semua fitur Kotlin, berkat itu ia mendapatkan begitu banyak penggemar.
Dalam artikel itu, saya akan berbicara tentang pengalaman migrasi aplikasi Android dari Jawa ke Kotlin, kesulitan yang harus diatasi dalam proses, dan menjelaskan mengapa semua ini tidak sia-sia. Artikel ini lebih ditujukan untuk pengembang Android yang baru mulai mempelajari Kotlin, dan, selain pengalaman pribadi, mengacu pada bahan-bahan anggota masyarakat lainnya.
Kenapa Kotlin?
Jelaskan secara singkat fitur-fitur Kotlin, karena itu saya beralih ke proyek, meninggalkan dunia Jawa yang "nyaman dan sangat akrab":
- Kompatibilitas Java Penuh
- Keamanan kosong
- Ketikkan inferensi
- Metode penyuluhan
- Berfungsi sebagai objek kelas dan lambda
- Generik
- Coroutine
- Tidak ada pengecualian yang diperiksa
Aplikasi DISCO
Ini adalah aplikasi dalam ukuran kecil untuk pertukaran kartu diskon, yang terdiri dari 10 layar. Menggunakan contohnya, kami akan mempertimbangkan migrasi.
Secara singkat tentang arsitektur
Aplikasi ini menggunakan arsitektur MVVM dengan Komponen Arsitektur Google di bawah tenda: ViewModel, LiveData, Room.
Juga, sesuai dengan prinsip-prinsip Arsitektur Bersih dari Paman Bob, saya memilih 3 lapisan dalam aplikasi: data, domain, dan presentasi.
Di mana untuk memulai? Jadi, kami membayangkan fitur-fitur utama Kotlin dan memiliki gagasan minimal tentang proyek yang perlu dimigrasi. Pertanyaan alami adalah "harus mulai dari mana?".
Halaman resmi
Memulai dengan Kotlin Android dokumentasi mengatakan bahwa jika Anda ingin port aplikasi yang sudah ada ke Kotlin, Anda hanya perlu mulai menulis unit test. Saat Anda mendapatkan sedikit pengalaman dengan bahasa ini, tulis kode baru di Kotlin, Anda hanya perlu mengonversi kode Java yang ada.
Tetapi ada satu "tetapi". Memang, konversi sederhana biasanya (meskipun tidak selalu) memungkinkan Anda untuk mendapatkan kode yang berfungsi di Kotlin, namun, idiomnya meninggalkan banyak yang diinginkan. Selanjutnya saya akan memberi tahu Anda cara menghilangkan kesenjangan ini karena fitur yang disebutkan (dan tidak hanya) dari bahasa Kotlin.
Migrasi Lapisan
Karena aplikasi sudah berlapis, masuk akal untuk bermigrasi secara berlapis-lapis, mulai dari atas.
Urutan lapisan selama migrasi ditunjukkan pada gambar berikut:
Bukan kebetulan bahwa kami memulai migrasi tepatnya dari lapisan atas. Dengan demikian kami menyelamatkan diri dari menggunakan kode Kotlin dalam kode Java. Sebaliknya, kita membuat kode Kotlin dari lapisan atas menggunakan kelas Java dari lapisan bawah. Faktanya adalah bahwa Kotlin pada awalnya dirancang dengan mempertimbangkan kebutuhan untuk berinteraksi dengan Jawa. Kode Java yang ada dapat dipanggil dari Kotlin dengan cara alami. Kita dapat dengan mudah mewarisi dari kelas Java yang ada, mengaksesnya dan menerapkan penjelasan Java ke kelas dan metode Kotlin. Kode Kotlin juga dapat digunakan di Jawa tanpa terlalu banyak kesulitan, tetapi seringkali membutuhkan usaha ekstra, seperti menambahkan anotasi JVM. Dan mengapa konversi yang tidak perlu dalam kode Java, jika pada akhirnya itu akan tetap ditulis ulang di Kotlin?
Sebagai contoh, mari kita lihat generasi yang kelebihan beban.
Biasanya, jika Anda menulis fungsi Kotlin dengan nilai parameter default, itu hanya akan terlihat di Jawa sebagai tanda tangan lengkap dengan semua parameter. Jika Anda ingin memberikan beberapa kelebihan beban ke panggilan Java, Anda dapat menggunakan anotasi @JvmOverloads:
class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) { @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { ... } }
Untuk setiap parameter dengan nilai default, ini akan membuat satu kelebihan tambahan, yang memiliki parameter ini dan semua parameter di sebelah kanannya, dalam daftar parameter jarak jauh. Dalam contoh ini, berikut ini akan dibuat:
Ada banyak contoh menggunakan anotasi JVM untuk operasi Kotlin yang benar.
Halaman dokumentasi ini merinci panggilan ke Kotlin dari Jawa.
Sekarang kami menggambarkan proses migrasi lapis demi lapis.
Lapisan Presentasi
Ini adalah lapisan antarmuka pengguna yang berisi layar dengan tampilan dan ViewModel, pada gilirannya, berisi properti dalam bentuk LiveData dengan data dari model. Selanjutnya, kami melihat trik dan alat yang ternyata berguna ketika memigrasi lapisan aplikasi ini.
1. Kapt prosesor pengolah anotasi
Seperti halnya MVVM apa pun, View terikat ke properti ViewModel melalui penyatuan data. Dalam kasus Android, kami berurusan dengan Android Databind Library, yang menggunakan pemrosesan anotasi. Jadi Kotlin memiliki
prosesor anotasi sendiri , dan jika Anda tidak membuat perubahan pada file build.gradle yang sesuai, proyek akan berhenti membangun. Karena itu, kami akan melakukan perubahan ini:
apply plugin: 'kotlin-kapt' android { dataBinding { enabled = true } } dependencies { api fileTree(dir: 'libs', include: ['*.jar'])
Penting untuk diingat bahwa Anda harus sepenuhnya mengganti semua kemunculan konfigurasi anotasiProcessor di build.gradle Anda dengan kapt.
Misalnya, jika Anda menggunakan perpustakaan Dagger atau Room dalam proyek, yang juga menggunakan prosesor anotasi di bawah tenda untuk pembuatan kode, Anda harus menetapkan kapt sebagai prosesor anotasi.
2. Fungsi sebaris
Saat menandai fungsi sebagai inline, kami meminta kompiler untuk meletakkannya di tempat penggunaan. Tubuh fungsi menjadi tertanam, dengan kata lain, diganti untuk penggunaan fungsi yang biasa. Berkat ini, kita dapat menghindari pembatasan penghapusan tipe, yaitu menghapus tipe. Saat menggunakan fungsi sebaris, kita bisa mendapatkan tipe (kelas) dalam runtime.
Fitur Kotlin ini digunakan dalam kode saya untuk "mengekstrak" kelas dari Kegiatan yang diluncurkan.
inline fun <reified T : Activity> Context?.startActivity(args: Bundle) { this?.let { val intent = Intent(this, T::class.java) intent.putExtras(args) it.startActivity(intent) } }
reified - sebutan untuk tipe reified.
Dalam contoh yang dijelaskan di atas, kami juga menyentuh fitur bahasa Kotlin seperti Extensions.
3. Ekstensi
Itu adalah ekstensi. Metode utilitas diambil dalam ekstensi, yang membantu untuk menghindari utilitas kelas yang membengkak dan mengerikan.
Saya akan memberikan contoh ekstensi yang terlibat dalam aplikasi:
fun Context.inflate(res: Int, parent: ViewGroup? = null): View { return LayoutInflater.from(this).inflate(res, parent, false) } fun <T> Collection<T>?.isNotNullOrEmpty(): Boolean { return this != null && isNotEmpty(); } fun Fragment.hideKeyboard() { view?.let { hideKeyboard(activity, it.windowToken) } }
Pengembang Kotlin memikirkan ekstensi Android yang berguna di muka dengan menawarkan plugin Kotlin Android Extensions mereka. Di antara fitur-fitur yang ia tawarkan adalah View binding dan dukungan Parcelable. Informasi terperinci tentang fitur-fitur plugin ini dapat ditemukan di
sini .
4. Fungsi Lambda dan fungsi urutan lebih tinggi
Menggunakan fungsi lambda dalam kode Android, Anda dapat menyingkirkan ClickListener dan panggilan balik yang canggung, yang di Jawa diimplementasikan melalui antarmuka yang ditulis sendiri.
Contoh menggunakan lambda alih-alih onClickListener:
button.setOnClickListener({ doSomething() })
Lambdas juga digunakan dalam fungsi tingkat tinggi, misalnya, untuk fungsi pengumpulan.
Ambil
peta sebagai contoh:
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {...}
Ada tempat di kode saya di mana saya perlu "memetakan" kartu id untuk penghapusan berikutnya.
Menggunakan ekspresi lambda yang diteruskan ke peta, saya mendapatkan array id yang diinginkan:
val ids = cards.map { it.id }.toIntArray() cardDao.deleteCardsByIds(ids)
Harap perhatikan bahwa tanda kurung dapat dihilangkan sama sekali saat memanggil fungsi, jika lambda adalah satu-satunya argumen, dan kata kunci itu adalah nama implisit dari satu-satunya parameter.
5. Jenis platform
Anda pasti harus bekerja dengan SDK yang ditulis dalam Java (termasuk, pada kenyataannya, Android SDK). Ini berarti Anda harus selalu waspada dengan Kotlin dan Java Interop seperti jenis platform.
Tipe platform adalah tipe yang Kotlin tidak dapat menemukan informasi validitas nol. Faktanya adalah bahwa, secara default, kode Java tidak mengandung informasi tentang validitas null, dan
NotNull dan @ Nullable penjelasan tidak selalu digunakan. Ketika tidak ada penjelasan yang sesuai di Jawa, tipe menjadi platform. Anda bisa bekerja dengannya baik sebagai tipe yang memungkinkan null, dan sebagai tipe yang tidak mengizinkan null.
Ini berarti seperti di Jawa, pengembang bertanggung jawab penuh untuk operasi dengan tipe ini. Compiler tidak menambahkan runtime cek nol dan akan memungkinkan Anda untuk melakukan semuanya.
Dalam contoh berikut, kami menimpa onActivityResult dalam Aktivitas kami:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent{ super.onActivityResult(requestCode, resultCode, data) val randomString = data.getStringExtra("some_string") }
Dalam hal ini, data adalah tipe platform yang mungkin mengandung null. Namun, dari sudut pandang kode Kotlin, data tidak boleh nol dalam keadaan apa pun, dan terlepas dari apakah Anda menentukan jenis Intent sebagai nullable, Anda tidak akan menerima peringatan atau kesalahan dari kompiler, karena kedua versi tanda tangan valid . Tetapi karena menerima data yang tidak kosong tidak dijamin, karena dalam kasus dengan SDK Anda tidak dapat mengontrol ini, mendapatkan null dalam kasus ini akan mengarah ke NPE.
Juga, sebagai contoh, kita dapat membuat daftar tempat-tempat berikut untuk kemungkinan munculnya jenis platform:
- Service.onStartCommand (), di mana Intent mungkin nol.
- BroadcastReceiver.onReceive ().
- Activity.onCreate (), Fragment.onViewCreate () dan metode serupa lainnya.
Selain itu, kebetulan bahwa parameter metode ini dijelaskan, tetapi untuk beberapa alasan studio kehilangan Nullability ketika menghasilkan override.
Lapisan Domain
Lapisan ini mencakup semua logika bisnis, yang bertanggung jawab atas interaksi antara lapisan data dan lapisan presentasi. Peran kunci di sini dimainkan oleh Repositori. Dalam Repositori, kami melakukan manipulasi data yang diperlukan, baik sisi server maupun lokal. Di lantai atas, ke lapisan Presentasi, kami hanya memberikan metode antarmuka Repositori, yang menyembunyikan kerumitan operasi data.
Seperti yang dinyatakan di atas, RxJava digunakan untuk implementasi.
1. RxJava
Kotlin sepenuhnya kompatibel dengan RxJava dan lebih ringkas dalam hubungannya dengan itu daripada Java. Namun, bahkan di sini saya harus menghadapi satu masalah yang tidak menyenangkan. Kedengarannya seperti ini: jika Anda melewatkan lambda sebagai parameter dari metode
andThen , lambda ini tidak akan berfungsi!
Untuk memverifikasi ini, cukup tulis tes sederhana:
Completable .fromCallable { cardRepository.uploadDataToServer() } .andThen { cardRepository.markLocalDataAsSynced() } .subscribe()
Dan kemudian konten
akan gagal. Ini adalah kasus dengan sebagian besar operator (seperti
flatMap ,
defer ,
fromAction dan banyak lainnya), benar-benar lambda diharapkan sebagai argumen. Dan dengan catatan seperti itu dengan
andThen ,
Completable / Observable / SingleSource diharapkan . Masalahnya diselesaikan dengan menggunakan tanda kurung biasa () alih-alih keriting {}.
Masalah ini dijelaskan secara rinci dalam artikel
βKotlin dan Rx2. Betapa aku menghabiskan 5 jam karena kurung yang salah .
"2. Restrukturisasi
Kami juga menyentuh sintaks Kotlin yang menarik seperti
penugasan perusakan atau
perusakan . Ini memungkinkan Anda untuk menetapkan objek ke beberapa variabel sekaligus, memecahnya menjadi beberapa bagian.
Bayangkan kita memiliki metode dalam API yang mengembalikan beberapa entitas sekaligus:
@GET("/foo/api/sync") fun getBrandsAndCards(): Single<BrandAndCardResponse> data class BrandAndCardResponse(@SerializedName("cards") val cards: List<Card>?, @SerializedName("brands") val brands: List<Brand>?)
Cara ringkas untuk mengembalikan hasil dari metode ini adalah merusak, seperti yang ditunjukkan dalam contoh berikut:
syncRepository.getBrandsAndCards() .flatMapCompletable {it-> Completable.fromAction{ val (cards, brands) = it syncCards(cards) syncBrands(brands) } } }
Perlu disebutkan bahwa multi-deklarasi didasarkan pada konvensi: kelas yang seharusnya dihancurkan harus mengandung fungsi componentN (), di mana N adalah nomor komponen yang sesuai - anggota kelas. Artinya, contoh di atas diterjemahkan ke dalam kode berikut:
val cards = it.component1() val brands = it.component2()
Contoh kami menggunakan kelas data yang secara otomatis mendeklarasikan fungsi componentN (). Oleh karena itu, multi-deklarasi bekerja dengannya di luar kotak.
Kita akan berbicara lebih banyak tentang kelas data di bagian selanjutnya, dikhususkan untuk lapisan Data.
Lapisan data
Lapisan ini termasuk POJO untuk data dari server dan pangkalan, antarmuka untuk bekerja dengan data lokal dan data yang diterima dari server.
Untuk bekerja dengan data lokal, Room digunakan, yang memberi kami pembungkus yang nyaman untuk bekerja dengan database SQLite.
Tujuan pertama untuk migrasi, yang menunjukkan dirinya sendiri, adalah POJO, yang dalam kode Java standar adalah kelas massal dengan banyak bidang dan metode get / set yang sesuai. Anda dapat membuat POJO lebih ringkas dengan bantuan kelas Data. Satu baris kode akan cukup untuk menggambarkan entitas dengan beberapa bidang:
data class Card(val id:String, val cardNumber:String, val brandId:String,val barCode:String)
Selain keringkasan, kita mendapatkan:
- Mengganti metode equals () , hashCode (), dan toString () di bawah tenda. Menghasilkan yang sama untuk semua properti dari kelas data sangat nyaman saat menggunakan DiffUtil dalam adaptor yang menghasilkan tampilan untuk RecyclerView. Faktanya adalah bahwa DiffUtil membandingkan dua set data, dua daftar: yang lama dan yang baru, mencari tahu perubahan apa yang telah terjadi, dan menggunakan metode notifikasi yang memperbarui adaptor secara optimal. Dan biasanya, daftar item dibandingkan menggunakan sama dengan.
Dengan demikian, setelah menambahkan bidang baru ke kelas, kita tidak perlu menambahkannya sama dengan sehingga DiffUtil memperhitungkan bidang baru. - Kelas yang tidak bisa diubah
- Dukungan untuk nilai default, yang dapat diganti dengan penggunaan pola Builder.
Contoh:
data class Card(val id : Long = 0L, val cardNumber: String="99", val barcode: String = "", var brandId: String="1") val newCard = Card(id =1L,cardNumber = "123")
Berita baik lainnya: dengan kapt yang dikonfigurasi (seperti dijelaskan di atas), kelas Data berfungsi dengan baik dengan anotasi Kamar, yang memungkinkan Anda menerjemahkan semua entitas basis data ke dalam kelas Data. Kamar juga mendukung properti yang dapat dibatalkan. Benar, Room belum mendukung nilai default dari Kotlin, tetapi bug yang sesuai telah dilembagakan untuk ini.
Kesimpulan
Kami hanya memeriksa beberapa perangkap yang mungkin timbul selama migrasi dari Jawa ke Kotlin. Adalah penting bahwa, meskipun masalah muncul, terutama dengan kurangnya pengetahuan teoritis atau pengalaman praktis, mereka semua dapat dipecahkan.
Namun, kesenangan menulis kode ekspresif dan aman ringkas di Kotlin akan lebih dari melunasi semua kesulitan yang timbul pada jalur transisi. Saya dapat mengatakan dengan yakin bahwa contoh proyek DISCO tentu saja menegaskan hal ini.
Buku, tautan bermanfaat, sumber daya
- Landasan teoritis pengetahuan bahasa akan memungkinkan peletakan buku Kotlin in Action dari pencipta bahasa Svetlana Isakova dan Dmitry Zhemerov.
Laconicism, informativeness, cakupan topik yang luas, fokus pada pengembang Java dan ketersediaan versi Rusia menjadikannya yang terbaik dari manual yang mungkin pada awal pembelajaran bahasa. Saya mulai dengan dia. - Sumber Kotlin dengan developer.android.
- Panduan Kotlin dalam Bahasa Rusia
- Artikel yang luar biasa dari Konstantin Mikhailovsky, pengembang Android dari Genesis, tentang pengalaman beralih ke Kotlin.