Generik, objek, dan ekstensi

1. Pengantar

Selama beberapa dekade, programmer merancang beberapa fitur bahasa pemrograman untuk membantu Anda menulis kode yang lebih baik—menunjukkan ide yang sama dengan lebih sedikit kode, abstraksi untuk mengekspresikan ide yang kompleks, dan menulis kode yang mencegah developer lain membuat kesalahan secara tidak sengaja hanyalah beberapa contoh. Begitu pula dengan bahasa Kotlin, dan ada sejumlah fitur yang dimaksudkan untuk membantu developer menulis kode yang lebih ekspresif.

Sayangnya, fitur-fitur ini dapat membuat segalanya menjadi rumit jika ini adalah pemrograman pertama Anda. Meskipun mungkin terdengar berguna, tingkat kegunaannya dan masalah yang dapat dipecahkan mungkin tidak selalu jelas. Kemungkinan besar, Anda sudah melihat beberapa fitur yang digunakan di Compose dan library lainnya.

Meskipun tidak ada pengganti untuk pengalaman, codelab ini menampilkan beberapa konsep Kotlin yang dapat membantu Anda membuat struktur aplikasi yang lebih besar:

  • Generik
  • Berbagai jenis class (class enum dan class data)
  • Objek pendamping dan singleton
  • Fungsi dan properti ekstensi
  • Fungsi cakupan

Di akhir codelab ini, Anda akan memiliki pengetahuan yang lebih mendalam tentang kode yang telah Anda lihat di kursus ini, dan mempelajari beberapa contoh kapan Anda akan menemukan atau menggunakan konsep ini di aplikasi Anda sendiri.

Prasyarat

  • Pemahaman tentang konsep pemrograman berorientasi objek, termasuk pewarisan.
  • Cara menentukan dan mengimplementasikan antarmuka.

Yang akan Anda pelajari

  • Cara menentukan parameter jenis generik untuk class.
  • Cara membuat instance class generik.
  • Kapan class enum dan data digunakan.
  • Cara menentukan parameter jenis generik yang harus mengimplementasikan antarmuka.
  • Cara menggunakan fungsi cakupan untuk mengakses metode dan properti class.
  • Cara menentukan objek singleton dan objek pendamping untuk class.
  • Cara memperluas class yang ada dengan properti dan metode baru.

Yang akan Anda butuhkan

  • Browser web dengan akses ke Kotlin Playground.

2. Membuat class yang dapat digunakan kembali dengan generik

Misalnya, Anda sedang menulis aplikasi untuk kuis online, serupa dengan kuis yang Anda lihat di kursus ini. Sering kali ada beberapa jenis pertanyaan kuis, seperti mengisi titik-titik, atau benar atau salah. Setiap pertanyaan kuis dapat diwakili oleh class, dengan beberapa properti.

Teks pertanyaan dalam kuis dapat diwakili oleh string. Pertanyaan kuis juga perlu menampilkan jawabannya. Namun, jenis pertanyaan yang berbeda—seperti benar atau salah—mungkin perlu merepresentasikan jawaban menggunakan jenis data yang berbeda. Mari kita tentukan tiga jenis pertanyaan yang berbeda.

  • Pertanyaan mengisi titik-titik: Jawabannya adalah kata yang direpresentasikan oleh String.
  • Pertanyaan benar atau salah: Jawaban direpresentasikan oleh Boolean.
  • Soal matematika: Jawabannya adalah nilai numerik. Jawaban untuk soal aritmetika sederhana direpresentasikan oleh Int.

Selain itu, terlepas dari jenis pertanyaannya, pertanyaan kuis dalam contoh kami juga memiliki rating kesulitan. Rating kesulitan direpresentasikan oleh string dengan tiga kemungkinan nilai: "easy", "medium", atau "hard".

Menentukan class untuk merepresentasikan setiap jenis pertanyaan kuis:

  1. Buka Kotlin Playground.
  2. Di atas fungsi main(), tentukan class untuk pertanyaan mengisi titik-titik bernama FillInTheBlankQuestion, yang terdiri dari properti String untuk questionText, properti String untuk answer, dan properti String untuk difficulty.
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)
  1. Di bawah class FillInTheBlankQuestion, tentukan class lain bernama TrueOrFalseQuestion untuk pertanyaan benar atau salah, yang terdiri dari properti String untuk questionText, properti Boolean untuk answer, dan properti String untuk difficulty.
class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
  1. Terakhir, di bawah dua class lainnya, tentukan class NumericQuestion yang terdiri dari properti String untuk questionText, properti Int untuk answer, dan properti String untuk difficulty.
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)
  1. Lihat kode yang Anda tulis. Apakah Anda melihat pengulangan?
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)

class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)

Ketiga class tersebut memiliki properti yang sama persis: questionText, answer, dan difficulty. Satu-satunya perbedaan adalah jenis data properti answer. Anda mungkin berpikir bahwa solusi yang jelas adalah membuat class induk dengan questionText dan difficulty, dan setiap subclass menentukan properti answer.

Namun, menggunakan pewarisan memiliki masalah yang sama seperti di atas. Setiap kali menambahkan jenis pertanyaan baru, Anda harus menambahkan properti answer. Satu-satunya perbedaan adalah jenis data. Memiliki class induk Question yang tidak memiliki properti jawaban juga akan terlihat aneh.

Jika Anda ingin properti memiliki jenis data yang berbeda, pembuatan subclass bukanlah jawabannya. Sebaliknya, Kotlin menyediakan sesuatu yang disebut jenis generik, yang memungkinkan Anda memiliki satu properti yang dapat memiliki jenis data berbeda, bergantung pada kasus penggunaan tertentu.

Apa yang dimaksud dengan jenis data generik?

Jenis umum, atau disingkat generik, memungkinkan jenis data, seperti class, untuk menentukan jenis data placeholder yang tidak diketahui dan dapat digunakan dengan properti dan metodenya. Apa maksudnya?

Pada contoh di atas, alih-alih menentukan properti jawaban untuk setiap kemungkinan jenis data, Anda dapat membuat satu class untuk merepresentasikan pertanyaan apa pun, dan menggunakan nama placeholder untuk jenis data properti answer. Jenis data sebenarnya—String, Int, Boolean, dll.—ditentukan saat instance class tersebut dibuat. Di mana pun nama placeholder digunakan, jenis data yang diteruskan ke class akan digunakan. Sintaksis untuk menentukan jenis generik suatu class ditampilkan di bawah ini:

67367d9308c171da.png

Jenis data generik disediakan saat membuat instance class, sehingga perlu ditetapkan sebagai bagian dari tanda tangan class. Nama class harus diikuti tanda kurung sudut yang menghadap ke kiri (<), diikuti dengan nama placeholder untuk jenis data, diikuti dengan tanda kurung sudut yang menghadap ke kanan (>).

Nama placeholder kemudian dapat digunakan di mana pun Anda menggunakan jenis data sebenarnya dalam class, seperti untuk properti.

81170899b2ca0dc9.png

Nama ini sama dengan deklarasi properti lainnya, tetapi yang digunakan adalah nama placeholder, bukan jenis data.

Bagaimana class Anda pada akhirnya akan mengetahui jenis data mana yang akan digunakan? Jenis data yang digunakan oleh jenis generik akan diteruskan sebagai parameter dalam tanda kurung siku saat Anda membuat instance class.

9b8fce54cac8d1ea.png

Nama class harus diikuti dengan tanda kurung sudut yang menghadap ke kiri (<), lalu jenis data sebenarnya seperti String, Boolean, Int, dan lainnya, kemudian tanda kurung sudut yang menghadap ke kanan (>). Jenis data nilai yang Anda teruskan untuk properti generik harus cocok dengan jenis data dalam tanda kurung sudut. Anda akan membuat properti jawaban generik agar dapat menggunakan satu class untuk merepresentasikan semua jenis pertanyaan kuis, baik jawabannya adalah String, Boolean, Int, maupun jenis data arbitrer.

Memfaktorkan ulang kode untuk menggunakan generik

Faktorkan ulang kode Anda untuk menggunakan satu class bernama Question dengan properti jawaban generik.

  1. Hapus definisi class untuk FillInTheBlankQuestion, TrueOrFalseQuestion, dan NumericQuestion.
  2. Buat class baru bernama Question.
class Question()
  1. Setelah nama class, tetapi sebelum tanda kurung, tambahkan parameter jenis generik menggunakan tanda kurung sudut yang menghadap ke kiri dan ke kanan. Panggil jenis generik T.
class Question<T>()
  1. Tambahkan properti questionText, answer, dan difficulty. questionText harus berjenis String. answer harus berjenis T karena jenis datanya ditentukan saat membuat instance class Question. Properti difficulty harus berjenis String.
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: String
)
  1. Untuk melihat cara kerjanya dengan beberapa jenis pertanyaan—mengisi titik-titik, benar atau salah, dll.—buat tiga instance class Question di main(), seperti yang ditunjukkan di bawah.
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
    val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
    val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}
  1. Jalankan kode Anda untuk memastikan semua berfungsi dengan benar. Sekarang Anda memiliki tiga instance dari class Question—masing-masing dengan jenis data berbeda untuk jawaban—bukan tiga class berbeda, atau tidak menggunakan pewarisan. Jika ingin menangani pertanyaan dengan jenis jawaban yang berbeda, Anda dapat menggunakan kembali class Question yang sama.

3. Menggunakan class enum

Di bagian sebelumnya, Anda sudah menentukan properti tingkat kesulitan dengan tiga kemungkinan nilai: "mudah", "sedang", dan "sulit". Meskipun cara ini berhasil, ada beberapa masalah.

  1. Jika Anda tidak sengaja salah mengetik salah satu dari tiga kemungkinan string, Anda dapat memunculkan bug.
  2. Jika nilainya berubah, misalnya "medium" diganti namanya menjadi "average", Anda perlu mengupdate semua penggunaan string.
  3. Tidak ada yang mencegah Anda atau developer lain secara tidak sengaja menggunakan string lain yang bukan salah satu dari tiga nilai yang valid.
  4. Kode akan lebih sulit dipertahankan jika Anda menambahkan lebih banyak tingkat kesulitan.

Kotlin membantu Anda mengatasi masalah ini menggunakan jenis class khusus yang disebut class enum. Class enum digunakan untuk membuat jenis dengan serangkaian kemungkinan nilai yang terbatas. Di dunia nyata, misalnya, empat arah mata angin—utara, selatan, timur, dan barat—dapat direpresentasikan oleh class enum. Penggunaan arah tambahan tidak diperlukan, dan kode tidak akan memungkinkan penggunaan itu. Sintaksis untuk class enum ditampilkan di bawah ini.

f4bddb215eb52392.png

Setiap kemungkinan nilai enum disebut konstanta enum. Konstanta enum ditempatkan di dalam tanda kurung kurawal yang dipisahkan oleh koma. Konvensinya adalah menggunakan huruf besar untuk setiap huruf dalam nama konstanta.

Anda mengacu pada konstanta enum menggunakan operator titik.

f3cfa84c3f34392b.png

Menggunakan konstanta enum

Ubah kode Anda agar menggunakan konstanta enum, bukan String, untuk merepresentasikan tingkat kesulitan.

  1. Di bawah class Question, tentukan class enum yang disebut Difficulty.
enum class Difficulty {
    EASY, MEDIUM, HARD
}
  1. Di class Question, ubah jenis data properti difficulty dari String menjadi Difficulty.
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. Saat melakukan inisialisasi tiga pertanyaan, teruskan konstanta enum untuk tingkat kesulitan.
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

4. Menggunakan class data

Banyak class yang telah Anda gunakan sejauh ini, seperti subclass Activity, memiliki beberapa metode untuk melakukan berbagai tindakan. Class ini tidak hanya merepresentasikan data, tetapi juga berisi banyak fungsi.

Di sisi lain, class seperti class Question hanya berisi data. Tidak ada metode apa pun yang melakukan tindakan. Class ini dapat ditetapkan sebagai class data. Menentukan class sebagai class data memungkinkan compiler Kotlin membuat asumsi tertentu dan menerapkan beberapa metode secara otomatis. Misalnya, toString() dipanggil di balik layar oleh fungsi println(). Saat Anda menggunakan class data, toString() dan metode lainnya akan diimplementasikan secara otomatis berdasarkan properti class.

Untuk menentukan class data, cukup tambahkan kata kunci data sebelum kata kunci class.

e7cd946b4ad216f4.png

Mengonversi Question ke class data

Pertama, Anda akan melihat apa yang terjadi saat mencoba memanggil metode seperti toString() pada class yang bukan merupakan class data. Kemudian, Anda akan mengonversi Question menjadi class data sehingga metode ini dan metode lainnya akan diimplementasikan secara default.

  1. Di main(), cetak hasil panggilan toString() di question1.
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
    println(question1.toString())
}
  1. Jalankan kode. Output hanya menampilkan nama class dan ID unik untuk objek.
Question@37f8bb67
  1. Buat Question menjadi class data menggunakan kata kunci data.
data class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. Jalankan lagi kode. Dengan menandai ini sebagai class data, Kotlin dapat menentukan cara menampilkan properti class saat memanggil toString().
Question(questionText=Quoth the raven ___, answer=nevermore, difficulty=MEDIUM)

Jika class didefinisikan sebagai class data, metode berikut akan diimplementasikan.

  • equals()
  • hashCode(): Anda akan melihat metode ini saat menangani jenis koleksi tertentu.
  • toString()
  • componentN(): component1(), component2(), dll.
  • copy()

5. Menggunakan objek singleton

Ada banyak skenario ketika Anda ingin class hanya memiliki satu instance. Contoh:

  1. Statistik pemain dalam game seluler untuk pengguna saat ini.
  2. Berinteraksi dengan satu perangkat hardware, seperti mengirim audio melalui speaker.
  3. Objek untuk mengakses sumber data jarak jauh (seperti database Firebase).
  4. Autentikasi, dengan hanya satu pengguna yang dapat login dalam satu waktu.

Dalam skenario di atas, Anda mungkin perlu menggunakan class. Namun, Anda hanya perlu membuat satu instance dari class tersebut. Jika hanya ada satu perangkat hardware, atau hanya satu pengguna yang login dalam satu waktu, tidak akan ada alasan untuk membuat lebih dari satu instance. Memiliki dua objek yang mengakses perangkat hardware yang sama secara bersamaan dapat menyebabkan perilaku yang sangat aneh dan mengganggu.

Anda dapat menyampaikan dengan jelas dalam kode Anda bahwa objek hanya boleh memiliki satu instance dengan menentukannya sebagai singleton. Singleton adalah class yang hanya dapat memiliki satu instance. Kotlin menyediakan konstruksi khusus, yang disebut objek, yang dapat digunakan untuk membuat class singleton.

Menentukan objek singleton

645e8e8bbffbb5f9.png

Sintaksis objek serupa dengan sintaksis class. Cukup gunakan kata kunci object, bukan kata kunci class. Objek singleton tidak dapat memiliki konstruktor karena Anda tidak dapat membuat instance secara langsung. Sebagai gantinya, semua properti ditentukan dalam tanda kurung kurawal dan diberi nilai awal.

Beberapa contoh yang diberikan sebelumnya mungkin tidak terlihat jelas, terutama jika Anda belum menangani perangkat hardware tertentu atau menangani autentikasi dalam aplikasi Anda. Namun, Anda akan melihat objek singleton muncul ketika Anda terus mempelajari pengembangan Android. Mari kita lihat cara kerjanya dengan contoh sederhana menggunakan objek untuk status pengguna, yang hanya memerlukan satu instance.

Untuk kuis, sebaiknya Anda memiliki cara untuk memantau jumlah total pertanyaan, dan jumlah pertanyaan yang dijawab siswa sejauh ini. Anda hanya perlu satu instance class ini, jadi alih-alih mendeklarasikannya sebagai class, deklarasikan sebagai objek singleton.

  1. Buat objek bernama StudentProgress.
object StudentProgress {
}
  1. Untuk contoh ini, kami akan mengasumsikan ada total sepuluh pertanyaan, dan tiga di antaranya telah dijawab sejauh ini. Tambahkan dua properti Int: total dengan nilai 10, dan answered dengan nilai 3.
object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}

Mengakses objek singleton

Ingat bahwa Anda tidak dapat membuat instance objek singleton secara langsung? Lalu, bagaimana cara Anda mengakses propertinya?

Karena hanya ada satu instance StudentProgress pada satu waktu, Anda mengakses propertinya dengan merujuk ke nama objek itu sendiri, diikuti dengan operator titik (.), lalu nama properti.

1b610fd87e99fe25.png

Update fungsi main() untuk mengakses properti objek singleton.

  1. Di main(), tambahkan panggilan ke println() yang menghasilkan pertanyaan answered dan total dari objek StudentProgress.
fun main() {
    ...
    println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
  1. Jalankan kode untuk memastikan semua berfungsi.
...
3 of 10 answered.

Mendeklarasikan objek sebagai objek pendamping

Class dan objek di Kotlin dapat ditentukan di dalam jenis lain, dan dapat menjadi cara yang bagus untuk mengatur kode Anda. Anda dapat menentukan objek singleton di dalam class lain menggunakan objek pendamping. Objek pendamping memungkinkan Anda mengakses properti dan metodenya dari dalam class, jika properti dan metode objek adalah bagian dari class tersebut, memungkinkan sintaksis yang lebih ringkas.

Untuk mendeklarasikan objek pendamping, cukup tambahkan kata kunci companion sebelum kata kunci object.

68b263904ec55f29.png

Anda akan membuat class baru bernama Quiz untuk menyimpan pertanyaan kuis dan menjadikan StudentProgress sebagai objek pendamping class Quiz.

  1. Di bawah enum Difficulty, tentukan class baru bernama Quiz.
class Quiz {
}
  1. Pindahkan question1, question2, dan question3 dari main() ke class Quiz. Anda juga harus menghapus println(question1.toString()) jika Anda belum melakukannya.
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

}
  1. Pindahkan objek StudentProgress ke dalam class Quiz.
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

    object StudentProgress {
        var total: Int = 10
        var answered: Int = 3
    }
}
  1. Tandai objek StudentProgress dengan kata kunci companion.
companion object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}
  1. Update panggilan ke println() untuk mereferensikan properti dengan Quiz.answered dan Quiz.total. Meskipun properti ini dideklarasikan dalam objek StudentProgress, properti ini dapat diakses dengan notasi titik menggunakan nama class Quiz saja.
fun main() {
    println("${Quiz.answered} of ${Quiz.total} answered.")
}
  1. Jalankan kode untuk memverifikasi output.
3 of 10 answered.

6. Memperluas class dengan properti dan metode baru

Saat menangani Compose, Anda mungkin melihat beberapa sintaksis yang menarik saat menentukan ukuran elemen UI. Jenis numerik, seperti Double, tampaknya memiliki properti seperti dp dan sp yang menentukan dimensi.

a25c5a0d7bb92b60.png

Mengapa desainer bahasa Kotlin menyertakan properti dan fungsi pada jenis data bawaan, terutama untuk membangun UI Android? Apakah mereka mampu memprediksi masa depan? Apakah Kotlin dirancang untuk digunakan dengan Compose, bahkan sebelum Compose ada?

Tentu saja tidak! Saat menulis class, Anda sering tidak tahu persis bagaimana developer lain akan menggunakannya, atau berencana untuk menggunakannya, di aplikasi mereka. Tidak mungkin memprediksi semua kasus penggunaan di masa mendatang, dan juga tidak disarankan untuk menambahkan penggelembungan yang tidak perlu ke kode Anda untuk beberapa kasus penggunaan yang tidak terduga.

Fungsi bahasa Kotlin adalah memberi developer lain kemampuan untuk memperluas jenis data yang ada, menambahkan properti dan metode yang dapat diakses dengan sintaksis titik, seolah-olah merupakan bagian dari jenis data tersebut. Developer yang tidak menangani jenis floating point di Kotlin, misalnya seseorang yang membangun library Compose, dapat memilih untuk menambahkan properti dan metode khusus ke dimensi UI.

Karena Anda telah melihat sintaksis ini saat mempelajari Compose di dua unit pertama, kini saatnya Anda mempelajari cara kerjanya di balik layar. Anda akan menambahkan beberapa properti dan metode untuk memperluas jenis yang ada.

Menambahkan properti ekstensi

Untuk menentukan properti ekstensi, tambahkan nama jenis dan operator titik (.) sebelum nama variabel.

1e8a52e327fe3f45.png

Anda akan memfaktorkan ulang kode di fungsi main() untuk mencetak progres kuis dengan properti ekstensi.

  1. Di bawah class Quiz, tentukan properti ekstensi Quiz.StudentProgress yang bernama progressText dari jenis String.
val Quiz.StudentProgress.progressText: String
  1. Tentukan pengambil untuk properti ekstensi yang menampilkan string yang sama dengan yang digunakan sebelumnya di main().
val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. Ganti kode dalam fungsi main() dengan kode yang mencetak progressText. Karena properti ini merupakan properti ekstensi objek pendamping, Anda dapat mengaksesnya dengan notasi titik menggunakan nama class, Quiz.
fun main() {
    println(Quiz.progressText)
}
  1. Jalankan kode untuk memverifikasi bahwa kode berfungsi.
3 of 10 answered.

Menambahkan fungsi ekstensi

Untuk menentukan fungsi ekstensi, tambahkan nama jenis dan operator titik (.) sebelum nama fungsi.

879ff2761e04edd9.png

Anda akan menambahkan fungsi ekstensi untuk menampilkan progres kuis sebagai status progres. Karena Anda tidak dapat membuat status progres di playground Kotlin, Anda akan mencetak status progres bergaya retro menggunakan teks.

  1. Tambahkan fungsi ekstensi ke objek StudentProgress bernama printProgressBar(). Fungsi ini tidak boleh menggunakan parameter dan tidak memiliki nilai return.
fun Quiz.StudentProgress.printProgressBar() {
}
  1. Cetak karakter , answered kali, menggunakan repeat(). Bagian status progres yang diarsir gelap merepresentasikan jumlah pertanyaan yang dijawab. Gunakan print() karena Anda tidak ingin baris baru setelah setiap karakter.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
}
  1. Cetak karakter , berapa kali yang sama dengan perbedaan antara total dan answered, menggunakan repeat(). Bagian yang diarsir terang merepresentasikan pertanyaan yang tersisa di panel proses.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
}
  1. Cetak baris baru menggunakan println() tanpa argumen, lalu cetak progressText.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. Update kode di main() untuk memanggil printProgressBar().
fun main() {
    Quiz.printProgressBar()
}
  1. Jalankan kode untuk memverifikasi output.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

Apakah Anda wajib melakukannya? Tentu saja tidak. Namun, memiliki opsi properti dan metode ekstensi akan memberi Anda lebih banyak opsi untuk mengekspos kode Anda kepada developer lain. Menggunakan sintaksis titik di jenis lain dapat membuat kode Anda lebih mudah dibaca, baik untuk Anda sendiri maupun developer lain.

7. Menulis ulang fungsi ekstensi menggunakan antarmuka

Di halaman sebelumnya, Anda telah melihat cara menambahkan properti dan metode ke objek StudentProgress tanpa menambahkan kode ke dalamnya secara langsung, menggunakan properti ekstensi dan fungsi ekstensi. Meskipun cara ini efektif untuk menambahkan fungsi ke satu class yang sudah ditentukan, terkadang Anda tidak perlu memperluas class jika memiliki akses ke kode sumber. Ada juga situasi saat Anda tidak tahu apa yang harus diimplementasikan, hanya metode atau properti tertentu yang harus ada. Jika Anda memerlukan beberapa class untuk memiliki properti dan metode tambahan yang sama, mungkin dengan perilaku yang berbeda, Anda dapat menentukan properti dan metode ini dengan antarmuka.

Misalnya, selain kuis, Anda juga memiliki class untuk survei, langkah dalam resep, atau data lain yang diurutkan yang dapat menggunakan status progres. Anda dapat menentukan antarmuka yang menetapkan metode dan/atau properti yang harus disertakan oleh setiap class ini.

eeed58ed687897be.png

Antarmuka ditentukan menggunakan kata kunci interface, diikuti dengan nama dalam bentuk UpperCamelCase, lalu kurung kurawal buka dan tutup. Dalam tanda kurung kurawal, Anda dapat menentukan tanda tangan metode atau properti get-only yang harus diimplementasikan oleh setiap class yang sesuai dengan antarmuka.

6b04a8f50b11f2eb.png

Antarmuka adalah kontrak. Class yang sesuai dengan antarmuka dikatakan memperluas antarmuka. Class dapat mendeklarasikan bahwa class tersebut ingin memperluas antarmuka menggunakan titik dua (:), diikuti dengan spasi, lalu nama antarmuka.

78af59840c74fa08.png

Sebagai hasilnya, class harus mengimplementasikan semua properti dan metode yang ditetapkan dalam antarmuka. Hal ini memungkinkan Anda dengan mudah memastikan bahwa setiap class yang perlu memperluas antarmuka akan mengimplementasikan metode yang sama persis dengan tanda tangan metode yang sama persis. Jika Anda memodifikasi antarmuka dengan cara apa pun, seperti menambahkan atau menghapus properti atau metode, atau mengubah tanda tangan metode, compiler mengharuskan Anda mengupdate class yang memperluas antarmuka, sehingga kode Anda tetap konsisten dan lebih mudah dikelola.

Antarmuka memungkinkan variasi perilaku class yang memperluasnya. Implementasi bergantung pada setiap class.

Mari kita lihat bagaimana Anda dapat menulis ulang status progres untuk menggunakan antarmuka, dan membuat class Kuis memperluas antarmuka tersebut.

  1. Di atas class Quiz, tentukan antarmuka bernama ProgressPrintable. Kami telah memilih nama ProgressPrintable karena membuat class yang memperluasnya dapat mencetak status progres.
interface ProgressPrintable {
}
  1. Di antarmuka ProgressPrintable, tentukan properti bernama progressText.
interface ProgressPrintable {
    val progressText: String
}
  1. Ubah deklarasi class Quiz untuk memperluas antarmuka ProgressPrintable.
class Quiz : ProgressPrintable {
    ...
}
  1. Di class Quiz, tambahkan properti bernama progressText dari jenis String, seperti yang ditentukan dalam antarmuka ProgressPrintable. Karena properti berasal dari ProgressPrintable, awali val dengan kata kunci penggantian.
override val progressText: String
  1. Salin pengambil properti dari properti ekstensi progressText lama.
override val progressText: String
        get() = "${answered} of ${total} answered"
  1. Hapus properti ekstensi progressText lama.

Kode yang akan dihapus:

val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. Di antarmuka ProgressPrintable, tambahkan metode bernama printProgressBar yang tidak menggunakan parameter dan tidak memiliki nilai yang ditampilkan.
interface ProgressPrintable {
    val progressText: String
    fun printProgressBar()
}
  1. Di class Quiz, tambahkan metode printProgressBar() menggunakan kata kunci override.
override fun printProgressBar() {
}
  1. Pindahkan kode dari fungsi ekstensi printProgressBar() lama ke printProgressBar() baru dari antarmuka. Ubah baris terakhir untuk merujuk ke variabel progressText baru dari antarmuka dengan menghapus referensi ke Quiz.
override fun printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(progressText)
}
  1. Hapus fungsi ekstensi printProgressBar(). Fungsi ini sekarang milik class Quiz yang memperluas ProgressPrintable.

Kode yang akan dihapus:

fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. Perbarui kode di main(). Karena fungsi printProgressBar() kini merupakan metode class Quiz, Anda harus membuat instance objek Quiz terlebih dahulu, lalu memanggil printProgressBar().
fun main() {
    Quiz().printProgressBar()
}
  1. Jalankan kode. Outputnya tidak berubah, tetapi kode Anda sekarang lebih modular. Seiring berkembangnya codebase, Anda dapat dengan mudah menambahkan class yang sesuai dengan antarmuka yang sama untuk menggunakan kembali kode tanpa mewarisi dari superclass.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

Ada banyak kasus penggunaan antarmuka untuk membantu menyusun kode dan Anda akan mulai sering menggunakannya di unit umum. Berikut adalah beberapa contoh antarmuka yang mungkin Anda temui saat terus menggunakan Kotlin.

  • Injeksi dependensi manual. Buat antarmuka yang menentukan semua properti dan metode dependensi. Mewajibkan antarmuka sebagai jenis data dependensi (aktivitas, kasus pengujian, dll.) sehingga instance dari setiap class yang mengimplementasikan antarmuka dapat digunakan. Hal ini memungkinkan Anda menukar implementasi yang mendasarinya.
  • Tiruan untuk pengujian otomatis. Baik class tiruan maupun class asli sesuai dengan antarmuka yang sama.
  • Mengakses dependensi yang sama di aplikasi Compose Multiplatform. Misalnya, buat antarmuka yang menyediakan serangkaian properti dan metode umum untuk Android dan desktop, meskipun implementasi yang mendasarinya berbeda untuk setiap platform.
  • Beberapa jenis data di Compose, seperti Modifier, adalah antarmuka. Hal ini memungkinkan Anda menambahkan pengubah baru tanpa perlu mengakses atau mengubah kode sumber yang mendasarinya.

8. Menggunakan fungsi cakupan untuk mengakses properti dan metode class

Seperti yang sudah Anda lihat, Kotlin menyertakan banyak fitur untuk membuat kode lebih ringkas.

Salah satu fitur yang akan Anda temui ketika Anda terus mempelajari pengembangan Android adalah fungsi cakupan. Fungsi cakupan memungkinkan Anda mengakses properti dan metode dari class secara singkat tanpa harus berulang kali mengakses nama variabel. Apa maksudnya? Mari kita lihat contoh berikut.

Menghilangkan referensi objek berulang dengan fungsi cakupan

Fungsi cakupan adalah fungsi tingkat tinggi yang memungkinkan Anda mengakses properti dan metode objek tanpa merujuk pada nama objek. Ini disebut fungsi cakupan karena isi fungsi yang diteruskan mengambil cakupan objek yang digunakan untuk memanggil fungsi cakupan. Misalnya, beberapa fungsi cakupan memungkinkan Anda mengakses properti dan metode dalam sebuah class, seolah-olah fungsi tersebut didefinisikan sebagai metode class tersebut. Hal ini dapat membuat kode Anda lebih mudah dibaca dengan memungkinkan Anda menghapus nama objek saat menyertakannya berlebihan.

Untuk menggambarkan hal ini dengan lebih baik, mari kita lihat beberapa fungsi cakupan berbeda yang akan Anda temui nanti dalam kursus.

Ganti nama objek panjang menggunakan let()

Fungsi let() memungkinkan Anda merujuk ke objek dalam ekspresi lambda menggunakan ID it, bukan nama objek yang sebenarnya. Hal ini dapat membantu Anda menghindari penggunaan nama objek yang lebih deskriptif dan panjang berulang kali ketika mengakses lebih dari satu properti. Fungsi let() adalah fungsi ekstensi yang dapat dipanggil pada objek Kotlin apa pun menggunakan notasi titik.

Coba akses properti question1, question2, dan question3 menggunakan let():

  1. Tambahkan fungsi ke class Quiz yang bernama printQuiz().
fun printQuiz() {

}
  1. Tambahkan kode berikut yang mencetak questionText, answer, dan difficulty pertanyaan. Meskipun beberapa properti diakses untuk question1, question2, dan question3, seluruh nama variabel digunakan setiap waktu. Jika nama variabel berubah, Anda perlu mengupdate setiap penggunaan.
fun printQuiz() {
    println(question1.questionText)
    println(question1.answer)
    println(question1.difficulty)
    println()
    println(question2.questionText)
    println(question2.answer)
    println(question2.difficulty)
    println()
    println(question3.questionText)
    println(question3.answer)
    println(question3.difficulty)
    println()
}
  1. Masukkan kode yang mengakses properti questionText, answer, dan difficulty dengan panggilan ke fungsi let() di question1, question2, dan question3. Ganti nama variabel di setiap ekspresi lambda dengannya.
fun printQuiz() {
    question1.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question2.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question3.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
}
  1. Perbarui kode di main() untuk membuat instance class Quiz yang bernama quiz.
fun main() {
    val quiz = Quiz()
}
  1. Panggil printQuiz().
fun main() {
    val quiz = Quiz()
    quiz.printQuiz()
}
  1. Jalankan kode untuk memastikan semua berfungsi.
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

Memanggil metode objek tanpa variabel menggunakan apply()

Salah satu fitur keren dari fungsi cakupan adalah Anda dapat memanggilnya pada objek bahkan sebelum objek tersebut ditetapkan ke variabel. Misalnya, fungsi apply() adalah fungsi ekstensi yang dapat dipanggil pada objek menggunakan notasi titik. Fungsi apply() juga menampilkan referensi ke objek tersebut sehingga dapat disimpan dalam variabel.

Update kode di main() untuk memanggil fungsi apply().

  1. Panggil apply() setelah kurung tutup saat membuat instance class Quiz. Anda dapat menghilangkan tanda kurung saat memanggil apply(), dan menggunakan sintaksis lambda di akhir.
val quiz = Quiz().apply {
}
  1. Pindahkan panggilan ke printQuiz() di dalam ekspresi lambda. Anda tidak perlu lagi mereferensikan variabel quiz atau menggunakan notasi titik.
val quiz = Quiz().apply {
    printQuiz()
}
  1. Fungsi apply() menampilkan instance class Quiz, tetapi karena Anda tidak lagi menggunakannya di mana pun, hapus variabel quiz. Dengan fungsi apply(), Anda bahkan tidak memerlukan variabel untuk memanggil metode pada instance Quiz.
Quiz().apply {
    printQuiz()
}
  1. Jalankan kode. Perhatikan bahwa Anda dapat memanggil metode ini tanpa referensi ke instance Quiz. Fungsi apply() menampilkan objek yang disimpan di quiz.
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

Meskipun penggunaan fungsi cakupan tidak wajib untuk mencapai output yang diinginkan, contoh di atas menggambarkan bagaimana fungsi tersebut dapat membuat kode Anda lebih ringkas dan menghindari pengulangan nama variabel yang sama.

Kode di atas hanya menunjukkan dua contoh, tetapi Anda disarankan untuk mem-bookmark dan membaca dokumentasi Fungsi Cakupan ketika menemukan penggunaannya nanti dalam kursus.

9. Ringkasan

Anda baru saja melihat cara kerja beberapa fitur baru Kotlin. Generik memungkinkan jenis data untuk diteruskan sebagai parameter ke class, class enum menentukan serangkaian kemungkinan nilai yang terbatas, dan class data membantu menghasilkan beberapa metode yang berguna untuk class secara otomatis.

Anda juga telah melihat cara membuat objek singleton—yang dibatasi untuk satu instance, cara menjadikannya sebagai objek pendamping class lain, dan cara memperluas class yang ada dengan properti get-only yang baru dan metode baru. Terakhir, Anda telah melihat beberapa contoh bagaimana fungsi cakupan dapat memberikan sintaksis yang lebih sederhana saat mengakses properti dan metode.

Anda akan melihat konsep ini di seluruh unit berikutnya saat Anda mempelajari lebih lanjut Kotlin, pengembangan Android, dan Compose. Anda kini memiliki pemahaman yang lebih baik tentang cara kerjanya dan bagaimana caranya dapat meningkatkan penggunaan kembali dan keterbacaan kode Anda.

10. Mempelajari lebih lanjut