Pemfaktoran ulang ke Kotlin

Dalam Codelab ini, Anda akan mempelajari cara mengonversi kode dari Java ke Kotlin. Anda juga akan mempelajari apa itu konvensi bahasa Kotlin dan cara memastikan bahwa kode yang ditulis mengikutinya.

Codelab ini cocok untuk semua developer yang menggunakan Java dan mempertimbangkan untuk memigrasikan project mereka ke Kotlin. Kita akan memulai tutorial ini dengan beberapa class Java yang akan dikonversi ke Kotlin dengan menggunakan IDE. Kemudian, kita akan melihat kode yang dikonversi dan juga melihat bagaimana kode tersebut dapat ditingkatkan dengan membuatnya lebih idiomatis dan menghindari kesalahan umum.

Yang akan Anda pelajari

Anda akan mempelajari cara mengonversi Java ke Kotlin. Untuk melakukannya, Anda akan mempelajari fitur dan konsep bahasa Kotlin berikut:

  • Menangani nullability
  • Mengimplementasikan singleton
  • Class data
  • Menangani string
  • Operator Elvis
  • Destrukturisasi
  • Properti dan properti pendukung
  • Argumen default dan parameter bernama
  • Menangani koleksi
  • Fungsi ekstensi
  • Fungsi dan parameter level atas
  • Kata kunci let, apply, with, dan run

Asumsi

Anda seharusnya sudah paham tentang Java.

Yang Anda butuhkan

Membuat project baru

Jika Anda menggunakan InteliJ IDEA, buat project Java baru dengan Kotlin/JVM.

Jika Anda menggunakan Android Studio, buat project baru tanpa Aktivitas.

Kode

Kita akan membuat objek model User dan class singleton Repository yang berfungsi dengan objek User dan mengekspos daftar pengguna serta nama pengguna yang telah diformat.

Buat file baru bernama User.java pada app/java/<yourpackagename> dan tempelkan kode berikut:

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

Bergantung pada jenis project Anda, impor androidx.annotation.Nullable jika Anda menggunakan project Android, atau org.jetbrains.annotations.Nullable jika tidak menggunakan project Android.

Buat file baru bernama Repository.java dan tempelkan kode berikut:

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

IDE dapat berfungsi dengan baik dalam memfaktorkan ulang kode Java secara otomatis menjadi kode Kotlin, tetapi terkadang diperlukan sedikit bantuan. Kita akan melakukan ini terlebih dahulu, lalu memeriksa kode yang difaktorkan ulang untuk memahami bagaimana dan mengapa kode tersebut difaktorkan ulang dengan cara ini.

Buka file User.java dan konversikan ke Kotlin: Menu bar -> Code -> Convert Java File to Kotlin File.

Jika IDE meminta untuk memperbaiki setelah konversi, tekan Yes.

Anda akan melihat kode Kotlin berikut:

class User(var firstName: String?, var lastName: String?)

Perhatikan bahwa User.java diganti namanya menjadi User.kt. File Kotlin memiliki ekstensi .kt.

Di class User Java, kita memiliki dua properti: firstName dan lastName. Setiap properti memiliki metode pengambil dan penyetel, sehingga nilainya dapat berubah. Kata kunci Kotlin untuk variabel yang dapat berubah adalah var, sehingga pengonversi menggunakan var untuk setiap properti ini. Jika properti Java hanya memiliki pengambil, properti tersebut tidak akan dapat berubah dan akan dideklarasikan sebagai variabel val. val mirip dengan kata kunci final di Java.

Salah satu perbedaan utama antara Kotlin dan Java adalah Kotlin secara eksplisit menentukan apakah variabel dapat menerima nilai null. Ini dilakukan dengan menambahkan `?` ke deklarasi jenis.

Karena kita telah menandai firstName dan lastName sebagai nullable, pengonversi secara otomatis akan menandai properti sebagai nullable dengan String?. Jika Anda memberi anotasi pada anggota Java sebagai non-null (menggunakan org.jetbrains.annotations.NotNull atau androidx.annotation.NonNull), pengonversi akan mengenalinya dan membuat kolom non-null di Kotlin.

Pemfaktoran ulang dasar sudah selesai. Namun, kita dapat menuliskannya dengan cara yang lebih idiomatis. Mari kita lihat caranya.

Class data

Class User hanya menyimpan data. Kotlin memiliki kata kunci class dengan peran ini: data. Dengan menandai class ini sebagai class data, compiler akan otomatis membuat pengambil dan penyetel untuk kita. Tindakan ini juga akan mendapatkan fungsi equals(), hashCode(), dan toString().

Mari kita tambahkan kata kunci data ke class User:

data class User(var firstName: String, var lastName: String)

Kotlin, seperti halnya Java, dapat memiliki konstruktor utama dan satu atau beberapa konstruktor sekunder. Konstruktor dalam contoh di atas adalah konstruktor utama class Pengguna. Jika Anda mengonversi class Java yang memiliki beberapa konstruktor, pengonversi juga akan otomatis membuat beberapa konstruktor di Kotlin. Keduanya ditentukan menggunakan kata kunci constructor.

Jika ingin membuat instance class ini, kita dapat melakukannya seperti ini:

val user1 = User("Jane", "Doe")

Persamaan

Kotlin memiliki dua jenis persamaan:

  • Persamaan struktural menggunakan operator == dan memanggil equals() untuk menentukan apakah dua instance tersebut sama.
  • Persamaan referensial menggunakan operator === dan memeriksa apakah dua referensi akan mengarah ke objek yang sama.

Properti yang ditentukan dalam konstruktor utama class data akan digunakan untuk pemeriksaan persamaan struktural.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

Di Kotlin, kita bisa menetapkan nilai default ke argumen dalam panggilan fungsi. Nilai default digunakan saat argumen dihilangkan. Di Kotlin, konstruktor juga merupakan fungsi, sehingga kita dapat menggunakan argumen default untuk menentukan bahwa nilai default lastName adalah null. Untuk melakukannya, kita cukup menetapkan null ke lastName.

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("John", "Doe")

Parameter fungsi dapat diberi nama saat memanggil fungsi:

val john = User(firstName = "John", lastName = "Doe") 

Sebagai kasus penggunaan yang berbeda, misalnya firstName memiliki null sebagai nilai defaultnya dan lastName tidak. Dalam hal ini, karena parameter default akan mendahului parameter tanpa nilai default, Anda harus memanggil fungsi dengan argumen bernama:

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

Sebelum melanjutkan, pastikan class User Anda adalah class data. Mari kita mengonversi class Repository ke Kotlin. Hasil konversi otomatis akan terlihat seperti ini:

import java.util.*

class Repository private constructor() {
   private var users: MutableList<User?>? = null
   fun getUsers(): List<User?>? {
       return users
   }

   val formattedUserNames: List<String?>
       get() {
           val userNames: MutableList<String?> =
               ArrayList(users!!.size)
           for (user in users) {
               var name: String
               name = if (user!!.lastName != null) {
                   if (user!!.firstName != null) {
                       user!!.firstName + " " + user!!.lastName
                   } else {
                       user!!.lastName
                   }
               } else if (user!!.firstName != null) {
                   user!!.firstName
               } else {
                   "Unknown"
               }
               userNames.add(name)
           }
           return userNames
       }

   companion object {
       private var INSTANCE: Repository? = null
       val instance: Repository?
           get() {
               if (INSTANCE == null) {
                   synchronized(Repository::class.java) {
                       if (INSTANCE == null) {
                           INSTANCE =
                               Repository()
                       }
                   }
               }
               return INSTANCE
           }
   }

   // keeping the constructor private to enforce the usage of getInstance
   init {
       val user1 = User("Jane", "")
       val user2 = User("John", null)
       val user3 = User("Anne", "Doe")
       users = ArrayList<Any?>()
       users.add(user1)
       users.add(user2)
       users.add(user3)
   }
}

Mari lihat apa yang dilakukan pengonversi otomatis:

  • Blok init telah ditambahkan (Repository.kt#L50)
  • Kolom static kini menjadi bagian dari blok companion object (Repository.kt#L33)
  • Daftar users adalah nullable karena objek tidak dibuat pada waktu deklarasi (Repository.kt#L7)
  • Metode getFormattedUserNames() kini menjadi properti yang disebut formattedUserNames (Repository.kt#L11)
  • Iterasi melalui daftar pengguna (yang awalnya merupakan bagian dari getFormattedUserNames() ) memiliki sintaksis yang berbeda dengan yang Java (Repository.kt#L15)

Sebelum dilanjutkan, mari kita bersihkan sedikit kodenya. Kita dapat melihat bahwa pengonversi membuat users daftar kita dapat diubah daftarnya yang menyimpan objek nullable. Meskipun daftarnya bisa berupa null, katakanlah daftar tersebut tidak dapat menampung pengguna null. Jadi, mari kita lakukan hal berikut:

  • Hapus ? di User? dalam deklarasi jenis users
  • getUsers akan menampilkan List<User>?

Pengonversi otomatis juga tidak perlu dibagi menjadi 2 baris deklarasi variabel dari variabel pengguna dan variabel yang ditentukan dalam blok init. Mari kita tempatkan setiap deklarasi variabel pada satu baris. Tampilan kode akan terlihat seperti berikut:

class Repository private constructor() {
    private var users: MutableList<User>? = null

    fun getUsers(): List<User>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Pemblokiran init

Di Kotlin, konstruktor utama tidak dapat berisi kode apa pun sehingga kode inisialisasi ditempatkan dalam blok init. Fungsinya sama.

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Sebagian besar kode init menangani inisialisasi properti. Hal ini juga dapat dilakukan di deklarasi properti. Misalnya, di versi Kotlin class Repository, kita melihat properti pengguna diinisialisasi di deklarasi.

private var users: MutableList<User>? = null

Properti dan metode static Kotlin

Di Java, kita menggunakan kata kunci static untuk kolom atau fungsi yang mengatakan bahwa kolom atau fungsi tersebut termasuk dalam suatu class, tetapi bukan instance dari class tersebut. Inilah alasan kita membuat kolom statis INSTANCE dalam class Repository. Persamaan Kotlin untuk ini adalah blok companion object. Di sini, Anda juga akan mendeklarasikan kolom statis dan fungsi statis. Pengonversi membuat dan memindahkan kolom INSTANCE di sini.

Menangani singleton

Karena hanya memerlukan satu instance dari class Repository, kita menggunakan pola singleton di Java. Dengan Kotlin, Anda dapat menerapkan pola ini di level compiler dengan mengganti kata kunci class dengan object.

Hapus konstruktor pribadi dan objek pendamping, lalu ganti definisi class dengan object Repository.

object Repository {

    private var users: MutableList<User>? = null

    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Saat menggunakan class object, kita cukup memanggil fungsi dan properti langsung pada objek, seperti ini:

val users = Repository.users

Destrukturisasi

Dengan Kotlin, objek dapat didestrukturisasi ke dalam sejumlah variabel menggunakan sintaksis yang disebut deklarasi destrukturisasi. Beberapa variabel telah dibuat dan dapat digunakan secara terpisah.

Misalnya, class data mendukung destrukturisasi sehingga kita dapat mendestrukturisasi objek User di loop for menjadi (firstName, lastName). Hal ini memungkinkan kita bekerja secara langsung dengan nilai firstName dan lastName. Mari kita perbarui loop for seperti ini:

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

Saat mengonversi class Repository ke Kotlin, pengonversi otomatis membuat daftar pengguna nullable, karena tidak diinisialisasi ke objek saat dideklarasikan. Untuk semua penggunaan objek users, operator pernyataan not-null !! akan digunakan. Fungsi ini mengonversi variabel apa pun menjadi jenis non-null dan melempar pengecualian jika nilainya null. Dengan menggunakan !!, kemungkinan besar pengecualian ditampilkan saat runtime.

Sebagai gantinya, lebih baik menangani nullability dengan menggunakan salah satu metode berikut:

  • Melakukan pemeriksaan null ( if (users != null) {...} )
  • Menggunakan operator elvis ?: (dibahas nanti di codelab)
  • Menggunakan beberapa fungsi standar Kotlin (dibahas nanti di codelab)

Dalam kasus ini, kita tahu bahwa daftar pengguna tidak harus menjadi nullable, karena daftar tersebut diinisialisasi langsung setelah objek dikonstruksi, sehingga kita bisa langsung membuat instance objek saat mendeklarasikannya.

Saat membuat instance jenis koleksi, Kotlin menyediakan beberapa fungsi bantuan untuk membuat kode lebih mudah dibaca dan fleksibel. Di sini kita menggunakan MutableList untuk users:

private var users: MutableList<User>? = null

Untuk memudahkan, kita dapat menggunakan fungsi mutableListOf(), memberikan jenis elemen daftar, menghapus panggilan konstruktor ArrayList dari blok init, dan menghapus deklarasi jenis eksplisit dari properti users.

private val users = mutableListOf<User>()

Kita juga mengubah var menjadi val karena pengguna akan berisi referensi yang tidak dapat diubah ke daftar pengguna. Perhatikan bahwa referensi tidak dapat diubah, tetapi daftar itu sendiri dapat diubah (Anda dapat menambahkan atau menghapus elemen).

Dengan perubahan tersebut, properti users kini adalah non-null, dan kita dapat menghapus semua kemunculan operator !! yang tidak perlu.

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

Selain itu, karena variabel pengguna sudah diinisialisasi, kita harus menghapus inisialisasi dari blok init:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

Karena lastName dan firstName dapat berupa null, kita harus menangani nullability saat membuat daftar nama pengguna yang telah diformat. Karena kita ingin menampilkan "Unknown" jika salah satu nama tidak ada, kita dapat membuat nama non-null dengan menghapus ? dari deklarasi jenis.

val name: String

Jika lastName adalah null, name adalah firstName atau "Unknown":

if (lastName != null) {
    if (firstName != null) {
        name = "$firstName $lastName"
    } else {
        name = lastName
    }
} else if (firstName != null) {
    name = firstName
} else {
    name = "Unknown"
}

Ini dapat ditulis secara lebih idiomatis dengan menggunakan operator elvis ?:. Operator elvis akan menampilkan ekspresi di sisi kiri jika bukan null, atau ekspresi di sisi kanan, jika sisi kiri adalah null.

Jadi, dalam kode berikut, user.firstName akan ditampilkan jika bukan null. Jika user.firstName adalah null, ekspresi ini akan menampilkan nilai di sebelah kanan , "Unknown":

if (lastName != null) {
    ...
} else {
    name = firstName ?: "Unknown"
}

Kotlin mempermudah penggunaan String dengan template String. Template string memungkinkan Anda mereferensikan variabel di dalam deklarasi string.

Pengonversi otomatis memperbarui penyambungan nama depan dan belakang untuk merujuk nama variabel secara langsung dalam string dengan menggunakan simbol $ dan menempatkan ekspresi di antara { } .

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

Dalam kode, ganti penyambungan String dengan:

name = "$firstName $lastName"

Di Kotlin, if, when, for, dan while adalah ekspresi dan akan menampilkan nilai. IDE Anda bahkan menampilkan peringatan bahwa penetapan harus dicabut dari if:

Mari ikuti saran IDE dan cabut tugas untuk kedua pernyataan if. Baris terakhir pernyataan if akan ditetapkan. Seperti ini, lebih jelas bahwa tujuan blok ini adalah menginisialisasi nilai nama:

name = if (lastName != null) {
    if (firstName != null) {
        "$firstName $lastName"
    } else {
        lastName
    }
} else {
   firstName ?: "Unknown"
}

Berikutnya, kita akan mendapatkan peringatan bahwa deklarasi name dapat digabungkan dengan tugas. Mari terapkan juga ini. Karena jenis variabel nama dapat disimpulkan, kita dapat menghapus deklarasi jenis eksplisit. Sekarang formattedUserNames terlihat seperti ini:

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(users.size)
       for ((firstName, lastName) in users) {
           val name = if (lastName != null) {
               if (firstName != null) {
                   "$firstName $lastName"
               } else {
                   lastName
               }
           } else {
               firstName ?: "Unknown"
           }
           userNames.add(name)
       }
       return userNames
   }

Mari kita pelajari tentang pengambil formattedUserNames lebih lanjut dan melihat cara membuatnya lebih idiomatis. Saat ini kode melakukan hal berikut:

  • Membuat daftar string baru
  • Melakukan iterasi melalui daftar pengguna
  • Membuat nama yang diformat untuk setiap pengguna, berdasarkan nama depan dan nama belakang pengguna
  • Menampilkan daftar yang baru dibuat
val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }

                userNames.add(name)
            }
            return userNames
        }

Kotlin menyediakan daftar lengkap transformasi koleksi sehingga pengembangan dapat dibuat lebih cepat dan lebih aman dengan memperluas kemampuan API Java Collections. Salah satunya adalah fungsi map. Fungsi ini akan menampilkan daftar baru yang berisi hasil penerapan fungsi transformasi yang diberikan ke setiap elemen yang ada dalam daftar asli. Jadi, sebagai ganti membuat daftar baru dan iterasi melalui daftar pengguna secara manual, kita dapat menggunakan fungsi map dan memindahkan logika yang sudah ada di loop for ke dalam isi map. Secara default, nama item daftar saat ini yang digunakan di map adalah it, tetapi Anda dapat mengganti it dengan nama variabel Anda sendiri agar mudah dibaca. Dalam kasus ini, mari kita beri nama user:

    
val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

Untuk lebih menyederhanakannya, kita dapat menghapus variabel name:

    
val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

Kita melihat bahwa pengonversi otomatis menggantikan fungsi getFormattedUserNames() dengan properti yang disebut formattedUserNames yang memiliki pengambil khusus. Di balik layar, Kotlin masih membuat metode getFormattedUserNames() yang menampilkan List.

Di Java, kita akan mengekspos properti class melalui fungsi pengambil dan penyetel. Kotlin memungkinkan Anda memiliki diferensiasi yang lebih baik antara properti class, yang diekspresikan dengan kolom, dan fungsionalitas, yaitu tindakan yang dapat dilakukan class, yang diekspresikan dengan fungsi. Dalam hal ini, class Repository sangat sederhana dan tidak melakukan tindakan apa pun sehingga hanya memiliki kolom.

Logika yang dipicu dalam fungsi getFormattedUserNames() Java kini akan dipicu saat memanggil pengambil properti Kotlin formattedUserNames.

Meskipun kita tidak secara eksplisit memiliki kolom yang sesuai dengan properti formattedUserNames, Kotlin menyediakan kolom pendukung otomatis bernama field yang dapat diakses dari pembuat dan penyetel kustom, jika diperlukan.

Namun, terkadang kita menginginkan beberapa fungsi tambahan yang tidak disediakan oleh kolom pendukung otomatis. Mari lihat contoh di bawah ini.

Dalam class Repository, kita memiliki daftar pengguna yang dapat berubah dan diekspos dalam fungsi getUsers() yang dihasilkan dari kode Java:

fun getUsers(): List<User>? {
    return users
}

Masalah yang ada di sini adalah dengan menampilkan users, konsumen class Repositori dapat mengubah daftar pengguna kita, dan itu bukanlah ide yang bagus. Mari kita perbaiki menggunakan properti pendukung.

Pertama, ganti nama users menjadi _users. Sekarang tambahkan properti publik yang tidak dapat diubah yang menampilkan daftar pengguna. Sebut saja users:

private val _users = mutableListOf<User>()
val users: List<User>
      get() = _users

Dengan perubahan ini, properti _users pribadi akan menjadi properti pendukung untuk properti users publik. Di luar class Repository, daftar _users tidak dapat diubah, karena konsumen class hanya dapat mengakses daftar melalui users.

Kode lengkap:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {

        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

Saat ini class Repository tahu cara menghitung nama pengguna yang diformat untuk objek User. Namun, jika ingin menggunakan kembali logika format yang sama di class lain, kita harus menyalin dan menempelkan logika tersebut atau memindahkannya ke class User.

Kotlin menyediakan kemampuan untuk mendeklarasikan fungsi dan properti di luar class, objek, atau antarmuka. Misalnya, fungsi mutableListOf() yang kita gunakan untuk membuat instance List baru ditentukan langsung di Collections.kt dari Standard Library.

Di Java, kapan pun Anda membutuhkan beberapa fungsi utilitas, Anda kemungkinan besar akan membuat class Util dan mendeklarasikan fungsi tersebut sebagai fungsi statis. Di Kotlin, Anda dapat mendeklarasikan fungsi level atas tanpa harus memiliki class. Namun, Kotlin juga menyediakan kemampuan untuk membuat fungsi ekstensi. Fungsi ekstensi adalah fungsi yang memperluas jenis tertentu, tetapi dideklarasikan di luar tipe. Dengan demikian, mereka memiliki afinitas untuk jenis tersebut.

Visibilitas fungsi dan properti ekstensi dapat dibatasi dengan menggunakan pengubah visibilitas. Cara ini akan membatasi penggunaan hanya untuk class yang memerlukan ekstensi, dan tidak mencemari namespace.

Untuk class User, kita dapat menambahkan fungsi ekstensi yang menghitung nama yang diformat, atau kita dapat menyimpan nama yang diformat di properti ekstensi. Ini dapat ditambahkan di luar class Repository, dalam file yang sama:

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

Kemudian, kita dapat menggunakan fungsi dan properti ekstensi seolah-olah keduanya adalah bagian dari class User.

Karena nama yang diformat adalah properti pengguna dan bukan fungsi class Repository, mari kita gunakan properti ekstensi. Sekarang file Repository terlihat seperti ini:

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {

        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

Library Standar Kotlin menggunakan fungsi ekstensi untuk memperluas fungsi beberapa API Java; banyak fungsi di Iterable dan Collection diimplementasikan sebagai fungsi ekstensi. Misalnya, fungsi map yang kita gunakan pada langkah sebelumnya adalah fungsi ekstensi di Iterable.

Dalam kode class Repository, kita menambahkan beberapa objek pengguna ke daftar _users. Panggilan ini dapat dibuat lebih unik dengan bantuan fungsi cakupan.

Untuk menjalankan kode hanya dalam konteks objek tertentu, tanpa perlu mengakses objek berdasarkan namanya, Kotlin membuat 5 fungsi cakupan: let, apply, with, run, dan also. Singkat dan kuat, semua fungsi ini memiliki penerima (this), dapat juga memiliki argumen (it), dan dapat menampilkan nilai. Anda harus memutuskan mana yang akan digunakan bergantung pada apa yang ingin dicapai.

Tips praktis yang berguna ini dapat membantu Anda mengingat hal ini:

Karena mengonfigurasikan objek _users di Repository, kita dapat membuat kode yang lebih idiomatis dengan menggunakan fungsi apply:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")
   
    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

Dalam codelab ini, kita telah membahas dasar-dasar yang diperlukan untuk mulai memfaktorkan ulang kode dari Java ke Kotlin. Pemfaktoran ulang ini tidak bergantung pada platform pengembangan dan membantu memastikan bahwa kode yang Anda tulis bersifat idiomatis.

Kotlin Idiomatis membuat penulisan kode menjadi singkat dan menarik. Dengan semua fitur yang disediakan oleh Kotlin, ada begitu banyak cara untuk membuat kode Anda menjadi lebih aman, lebih ringkas, dan lebih mudah dibaca. Misalnya, kita bahkan dapat mengoptimalkan class Repository dengan membuat instance daftar _users dengan pengguna secara langsung dalam deklarasi, sehingga blok init dapat dihilangkan:

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

Kita telah membahas beragam topik, dari menangani nullability, singleton, String, dan koleksi hingga topik seperti fungsi ekstensi, fungsi tingkat atas, properti, dan fungsi cakupan. Kita sudah beralih dari dua class Java ke dua class Kotlin yang sekarang terlihat seperti ini:

User.kt

class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

Berikut adalah TL;DR dari fungsi Java beserta pemetaannya ke Kotlin:

Java

Kotlin

Objek final

Objek val

equals()

==

==

===

Class yang hanya menyimpan data

Class data

Inisialisasi dalam konstruktor

Inisialisasi dalam blok init

Kolom dan fungsi static

kolom dan fungsi yang dideklarasikan dalam companion object

Class singleton

object

Untuk mencari tahu lebih lanjut tentang Kotlin dan juga cara menggunakannya di platform Anda, lihat referensi berikut: