שילוב מחדש של קוטלין

במעבדה זו תוכלו ללמוד איך להמיר את הקוד מ-Java ל-Kotlin. תלמדו גם מהן המוסכמות של קוטלין ואיך תוכלו לוודא שהקוד שאתם כותבים תואם להן.

Codelab זה מתאים לכל מפתח שמשתמש ב-Java ששוקלים להעביר את הפרויקט שלו ל-Kotlin. נתחיל במספר שיעורי Java שניתן להמיר לקוטלין באמצעות IDE. לאחר מכן, נבחן את הקוד שהומר ונבדוק איך אנחנו יכולים לשפר אותו על ידי הגדרתו דיאומטית ומניעת שגיאות נפוצות.

מה תלמדו

נסביר איך להמיר את Java ל-Kotlin. לשם כך, תלמדו את התכונות והקונספטים הבאים של השפה בקוטלין:

  • טיפול באפסיות (null)
  • הטמעת טונים אחידים
  • מחלקות נתונים
  • טיפול במחרוזות
  • האופרטור של Elvis
  • הרס
  • נכסים ונכסים לגיבוי
  • ארגומנטי ברירת מחדל ופרמטרים בעלי שם
  • עבודה עם אוספים
  • פונקציות של תוספים
  • פונקציות ופרמטרים ברמה עליונה
  • let, apply, with ו-run מילות מפתח

הנחות

אם אתם כבר מכירים את Java.

מה תצטרך להכין

יצירה של פרויקט חדש

אם אתם משתמשים ב- IntelliJ IDEA, צרו פרויקט Java חדש עם Kotlin/JVM.

אם אתם משתמשים ב-Android Studio, יש ליצור פרויקט חדש ללא פעילות.

הקוד

אנחנו ניצור אובייקט מודל מסוג User ומחלקה אחת (Repository) של ישות אחת שעובדת עם אובייקטים User וחושפת רשימות של משתמשים ושל שמות משתמשים בפורמט.

יוצרים קובץ חדש בשם User.java בקטע app/javascript/<yourpackagename> ומדביקים את הקוד הבא:

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

}

בהתאם לסוג הפרויקט, יש לייבא את androidx.annotation.Nullable אם משתמשים בפרויקט Android, או org.jetbrains.annotations.Nullable אם לא.

יוצרים קובץ חדש בשם Repository.java ומדביקים את הקוד הבא:

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 שלנו יכול לבצע עבודה טובה מחדש בהסבה מחדש של קוד Java לקוד של Kotlin, אבל לפעמים הוא צריך קצת עזרה. אנחנו עושים זאת קודם כל, ולאחר מכן עוברים על הקוד שהוקלד מחדש כדי להבין איך הן הוזכרו מחדש.

עוברים אל הקובץ User.java וממירים אותו ל-Kotlin: סרגל התפריטים -> קוד -> המרת קובץ Java לקובץ Kotlin.

אם קובץ ה-IDE מציג בקשה לתיקון לאחר ההמרה, לוחצים על כן.

אתם אמורים לראות את קוד Kotlin הזה:

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

לידיעתך, השם של User.java השתנה ל-User.kt. הסיומת kt של קובצי Kotlin מתבצעת.

בשיעור Java User שלנו, היו לנו שני נכסים: firstName ו-lastName. לכל אחד הייתה שיטת getter ו-setter, כך שהערך שלו משתנה. מילת המפתח של Kotlin למשתנים משתנים היא var, כך שהממיר משתמש ב-var לכל אחד מהנכסים האלו. אם נכסי ה-Java שלנו קיבלו רק משתנים, לא ניתן יהיה לשנות אותם והכריזו עליהם כמשתני val. val דומה למילת המפתח final ב-Java.

אחד ההבדלים העיקריים בין Kotlin ל-Java הוא ש-Kotlin מציינת באופן מפורש אם משתנה יכול לקבל ערך null. פעולה זו מתבצעת על ידי צירוף '?' להצהרת הסוג.

מאחר שסמןנו את firstName ואת lastName בתור null, המשתמש שהשלים המרה אוטומטית יסמן את המאפיינים בתור null עם String?. אם מוסיפים הערות למשתמשי Java בתור null (באמצעות org.jetbrains.annotations.NotNull או androidx.annotation.NonNull), הממיר יזהה את השדה ויהפוך את השדות שאינם Null ל-Kotlin גם.

החישוב מחדש כבר בוצע. אבל אנחנו יכולים לנסח את זה באופן אידוי יותר. בואו נראה איך.

סיווג נתונים

השיעור שלנו ב-User מכיל נתונים בלבד. קוטלין מכילה מילת מפתח בקורסים של תפקיד זה: data. כשמסמנים את הכיתה הזו ככיתה data, המהדר יוצר באופן אוטומטי getter ומגדירים. היא תציג גם את הפונקציות equals(), hashCode() ו-toString().

נוסיף את מילת המפתח data לכיתה User:

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

קוטלין, כמו Java, יכול להיות בנאי ראשי ובנייה משנית אחת או יותר. הדוגמה בדוגמה שלמעלה היא ה-constructor הראשי של מחלקת המשתמשים. אם אתם ממירים מחלקת Java שיש בה מספר בנאים, הממיר ייצור באופן אוטומטי גם מספר בונים ב-Kotlin. הם מוגדרים באמצעות מילת המפתח constructor.

אם אנחנו רוצים ליצור מופע של הכיתה הזו, ניתן לעשות זאת כך:

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

שוויון

לקוטלין יש שני סוגים של שוויון:

  • אי-שוויון מבני משתמש באופרטור == ומתקשר ל-equals() כדי לקבוע אם שני מופעים שווים.
  • שוויון הפניה משתמש באופרטור === ובודק אם שתי הפניות מפנות לאותו אובייקט.

הנכסים שמוגדרים בבונה הראשי של מחלקת הנתונים ישמשו לבדיקות שוויון מבניות.

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

ב-Cotlin, אנחנו יכולים להקצות ערכי ברירת מחדל לארגומנטים בקריאות לפונקציה. המערכת משתמשת בערך ברירת המחדל אחרי שהארגומנט מושמט. בקוטון, בנאים הם גם פונקציות, לכן אנחנו יכולים להשתמש בארגומנטים שמוגדרים כברירת מחדל כדי לציין שערך ברירת המחדל של lastName הוא null. לשם כך אנחנו מקצים את null ל-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")

ניתן לתת שם לפרמטרים של פונקציית במהלך קריאה לפונקציות:

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

כתרחיש לדוגמה אחר, נניח שהפקודה firstName היא null כברירת המחדל שלה, וlastName לא. במקרה הזה, מפני שפרמטר ברירת המחדל יקודם לפרמטר שאין לו ערך ברירת מחדל, תצטרכו לקרוא לפונקציה לפונקציה באמצעות ארגומנטים עם שם:

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

לפני שממשיכים, צריך לוודא שהכיתה שלך ב-User היא כיתה ב-data. יש להמיר את הכיתה מRepository לקוטלין. תוצאת ההמרה האוטומטית אמורה להיראות כך:

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

בואו נראה מה ביצע ההמרה האוטומטית:

  • נוסף בלוק init (Repository.kt#L50)
  • השדה static הוא חלק מבלוק companion object (Repository.kt#L33)
  • הרשימה של users ניתנת לביטול כי האובייקט לא נוצר בזמן ההצהרה (Repository.kt#L7)
  • השיטה של getFormattedUserNames() היא עכשיו נכס בשם formattedUserNames (Repository.kt#L11)
  • באיטרציה שבה מופיעה רשימת המשתמשים (שהיתה חלק מ-getFormattedUserNames() יש תחביר שונה מזה של Java (Repository.kt#L15)

לפני שנמשיך, כדאי לנקות את הקוד עוד קצת. ניתן לראות שהממיר הפך את רשימת users שלך לרשימת שינויים שכוללת אובייקטים שניתן לבטל. הרשימה אכן יכולה להיות ריקה, אבל אפשר לומר שהיא יכולה להחזיק משתמשים ריקים. אז אז...

  • יש להסיר את ? מUser? בתוך הצהרת הסוג users
  • getUsers צריך להחזיר List<User>?

בנוסף, הממיר האוטומטי מבצע פיצול שלא לצורך, בשתי שורות של הצהרות המשתנים של המשתנים של המשתמשים ושל המשתנים שהוגדרו בבלוק של הכניסה. צריך להוסיף כל הצהרת משתנה בשורה אחת. כך הקוד שלנו אמור להיראות:

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

בלוק של Init

ב-Kotlin, ה-constructor הראשי לא יכול להכיל קוד, לכן קוד האתחול מוצב בבלוקים של init. הפונקציונליות זהה.

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

חלק גדול מהקוד של init מטפל באתחול של נכסים. ניתן לעשות זאת גם בהצהרה של הנכס. לדוגמה, בגרסה של Kotlin של הכיתה Repository שלנו, אנחנו רואים שמאפיין המשתמש הופעל בהצהרה.

private var users: MutableList<User>? = null

נכסים ושיטות של staticKtlin'

ב-Java, אנחנו משתמשים במילת המפתח static בשדות או בפונקציות כדי לציין שהם שייכים לכיתה, אבל לא למופע של הכיתה. לכן יצרנו את השדה הסטטי INSTANCE בקורס שלנו Repository. המקבילה של Kotlin לערך הזה היא הבלוק companion object. בקטע הזה צריך גם להצהיר על השדות הסטטיים והפונקציות הסטטיות. הממיר יצר והעביר את השדה INSTANCE לכאן.

טיפול בטון יחיד

מכיוון שאנחנו צריכים רק מופע אחד של המחלקה Repository, השתמשנו ב-singleton template ב-Java. באמצעות Kotlin, ניתן לאכוף את הדפוס הזה ברמת המהדר על ידי החלפת מילת המפתח class ב-object.

יש להסיר את המבנה הפרטי ואת האובייקט הנלווה ולהחליף את הגדרת הכיתה ב-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)
    }
}

כשמשתמשים במחלקה object, אנחנו קוראים לפונקציות ולמאפיינים ישירות לגבי האובייקט, כך:

val users = Repository.users

השחתה

Kotlin מאפשר להשמיד אובייקט במספר משתנים באמצעות תחביר של הצהרת הרס. אנחנו יוצרים משתנים מרובים ומשתמשים בהם באופן עצמאי.

לדוגמה, מחלקת נתונים תומכת ב ניהול היררכיות כדי שנוכל להשמיד את האובייקט User בחלק for של (firstName, lastName). כך אנחנו יכולים לעבוד ישירות עם הערכים firstName ו-lastName. יש לעדכן את הלולאה של for כך:

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

בזמן ההמרה של המחלקה Repository ל-Kotlin, הממיר האוטומטי הפך את רשימת המשתמשים לחסרה, מפני שהיא לא הופעלה לאובייקט בעת ההכרזה. לכל השימושים באובייקט users, ייעשה שימוש באופרטור הטענה שאינו Null !!. היא ממירה כל משתנה לסוג שאינו Null ויוצרת חריגה אם הערך הוא null. השימוש ב-!! עלול לסכן את החריגים בזמן הריצה.

במקום זאת, אפשר להשתמש ב-אחת מהשיטות הבאות כדי לטפל באיפוס

  • מתבצעת בדיקה ריקה ( if (users != null) {...} )
  • שימוש באופרטור Elvis ?: (שמכוסה מאוחר יותר ב-Codelab)
  • שימוש בחלק מהפונקציות הסטנדרטיות של Kotlin (שמתוארות בהמשך ב-Codelab)

במקרה שלנו, אנחנו יודעים שרשימת המשתמשים לא חייבת להיות ריקה, כי היא מתחילה לפעול מיד אחרי שהאובייקט נוצר, כדי שנוכל לספק אובייקט באופן ישיר כאשר אנחנו מצהירים עליו.

בעת יצירת מופעים של סוגי אוספים, Kotlin מספק כמה פונקציות מסייעות להפיכת הקוד לקריא וגמיש יותר. כאן אנחנו משתמשים ב-MutableList עבור users:

private var users: MutableList<User>? = null

כדי לפשט את השימוש, אנחנו יכולים להשתמש בפונקציה mutableListOf(), לציין את סוג הרכיב של הרשימה, להסיר את הקריאה לבנייה של ArrayList מבלוק ה-init ולהסיר את הצהרת הסוג הבוטה של הנכס users.

private val users = mutableListOf<User>()

בנוסף, שינינו את הערך ל-val כי המשתמשים יכללו הפניה בלתי הפיכה לרשימת המשתמשים. שימו לב שההפניה אינה ניתנת לשינוי, אבל הרשימה עצמה משתנה (תוכלו להוסיף או להסיר רכיבים).

בעקבות השינויים האלה, הנכס שלנו ב-users אינו Null, ואנחנו יכולים להסיר את כל האירועים החסרים של אופרטור !!.

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

כמו כן, מכיוון שמשתנה המשתמשים כבר אותחל, אנחנו צריכים להסיר את האתחול מבלוק 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)
}

מאחר ש-lastName ו-firstName יכולים להיות null, עלינו לטפל בביטולים כאשר אנחנו יוצרים את רשימת שמות המשתמשים בפורמט הנכון. מכיוון שאנחנו רוצים להציג את "Unknown" אם אחד השמות לא נמצא, נוכל להגדיר את השם כ'לא ריק' על ידי הסרת ? מהצהרת הסוג.

val name: String

אם הערך של lastName הוא null, הערך name הוא firstName או "Unknown":

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

אפשר לכתוב את הטקסט הזה בצורה אידאלית באמצעות האופרטור elvis ?:. האופרטור elvis יחזיר את הביטוי מצד שמאל שלו אם הוא לא Null, או את הביטוי בצד ימין שלו, אם הצד השמאלי שלו הוא null.

לכן, הקוד הבא: user.firstName מוחזר אם הוא לא null. אם הערך user.firstName הוא null, הביטוי יחזיר את הערך מצד ימין , "Unknown":

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

עם Kotlin קל לעבוד עם String באמצעות תבניות מחרוזת. תבניות מחרוזת מאפשרות להפנות למשתנים בתוך הצהרות מחרוזת.

הממיר האוטומטי עדכן את שרשור השם הפרטי ושם המשפחה כך שיתייחס לשם המשתנה ישירות במחרוזת באמצעות הסמל $ ויוסיף את הביטוי בין { } .

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

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

בקוד, מחליפים את שרשור המחרוזת ב:

name = "$firstName $lastName"

בקוטלי (if), when, for ו-while הם ביטויים – הם מחזירים ערך. בסביבת הפיתוח המשולבת שלך מופיעה גם אזהרה על כך שיש להסיר את ההקצאה מהif:

בואו לעקוב אחר ההצעה של IDE' והסירו את ההקצאה של שתי ההצהרות if. השורה האחרונה של הצהרת ההצהרה תוקצה. כך ברור יותר שמטרת החסימה הזו היא להפעיל את ערך השם:

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

לאחר מכן תקבלו אזהרה על כך שאפשר לצרף את הצהרת name למטלה. גם היא עובדת. מאחר שניתן להסיק את סוג משתנה השם, אנחנו יכולים להסיר את הצהרת הסוג הבוטה. עכשיו נראה מכשיר ה-formattedUserNames שלנו:

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
   }

נבחן את הכלי הזה ב-formattedUserNames יותר לעומק כדי להבין איך לשפר אותו. בשלב הזה, הקוד מבצע את הפעולות הבאות:

  • יצירת רשימה חדשה של מחרוזות
  • חוזר ברשימת המשתמשים
  • בונה את השם המפורמט עבור כל משתמש, על סמך השם הפרטי ושם המשפחה של המשתמש
  • החזרת הרשימה החדשה שנוצרה
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 מספק רשימה מקיפה של טרנספורמציות באוסף שהופכות את הפיתוח למהיר ולבטוח יותר על ידי הרחבת היכולות של Java collections API. אחת מהן היא הפונקציה map. פונקציה זו מחזירה רשימה חדשה הכוללת את התוצאות של החלת פונקציית הטרנספורמציה הנתונה על כל רכיב ברשימה המקורית. לכן, במקום ליצור רשימה חדשה ולחזור על כל רשימת המשתמשים באופן ידני, אנחנו יכולים להשתמש בפונקציה map ולהעביר את הלוגיקה שהיו לנו בלולאה של for בתוך גוף map. כברירת מחדל, השם של פריט הרשימה הנוכחי המשמש ב-map הוא it, אבל כדי לשנות את הקריאה הזו, תוכלו להחליף את it בשם המשתנה שלכם. במקרה שלנו, תן שם ל-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
            }
        }

כדי לפשט את הנושא עוד יותר, אנחנו יכולים להסיר לחלוטין את המשתנה 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"
                }
            }
        }

שמנו לב שהממיר האוטומטי החליף את הפונקציה getFormattedUserNames() בנכס בשם formattedUserNames שיש לו getter מותאם אישית. מאחורי הקלעים, Kotlin עדיין יוצר שיטת getFormattedUserNames() שמחזירה List.

ב-Java, נחשוף את מאפייני הכיתה שלנו באמצעות פונקציות getter ו-Setter. קוטלין מאפשר לנו ליצור הבחנה טובה יותר בין מאפיינים של מחלקה, מתבטאת בשדות ובפונקציות, פעולות שמחלקה יכולה לבצע, מבוטאות בפונקציות. במקרה שלנו, המחלקה Repository פשוטה מאוד ולא מבצעת פעולות כלשהן כך שהיא תכיל שדות בלבד.

הלוגיקה שהופעלה בפונקציה Java getFormattedUserNames() מופעלת עכשיו בעת הפעלת getter של מאפיין formattedUserNames Kotlin.

אין לנו שדה מפורש המשויך לנכס formattedUserNames, אבל קוטלין מספק לנו שדה גיבוי אוטומטי בשם field , שאליו אנחנו יכולים לגשת, אם יהיה בכך צורך ממפתחים ומהגדרות בהתאמה אישית.

עם זאת, לפעמים אנחנו רוצים פונקציונליות נוספת ששדה הגיבוי האוטומטי לא מספק. לפניכם דוגמה להמחשה.

במסגרת הכיתה Repository, יש לנו רשימה מרובה של משתמשים שנחשפים בפונקציה getUsers(), שנוצרה מקוד Java שלנו:

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

הבעיה כאן היא שהחזרת users על ידי כל צרכן ממחזור המאגר יכולה לשנות את רשימת המשתמשים שלנו – לא רעיון טוב! תיקון הבעיה באמצעות נכס גיבוי.

תחילה, יש לשנות את השם של users ל-_users. עכשיו צריך להוסיף נכס ציבורי שאינו קבוע שמחזיר רשימה של משתמשים. זה הזמן להתקשר אל users:

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

בעקבות השינוי הזה, נכס _users הפרטי יהפוך לנכס הגיבוי של נכס users הגלוי לכול. מחוץ לקטגוריה של Repository, לא ניתן לשנות את הרשימה _users, כי לקוחות של הכיתה יכולים לגשת לרשימה רק דרך users.

קוד מלא:

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

נכון לעכשיו, הכיתה Repository יודעת איך לחשב את שם המשתמש המפורמט עבור אובייקט User. עם זאת, אם אנחנו רוצים להשתמש מחדש באותו לוגיקה של עיצוב בכיתות אחרות, עלינו להעתיק ולהדביק אותו או להעביר אותו לכיתה User.

Kotlin מאפשר להצהיר על פונקציות ומאפיינים מחוץ לכל מחלקה, אובייקט או ממשק. לדוגמה, הפונקציה mutableListOf() שבה השתמשנו כדי ליצור מופע חדש של List מוגדרת ישירות ב-Collections.kt מהספרייה הרגילה.

ב-Java, בכל פעם שתזדקקו לפונקציונליות כלשהי, תצטרכו ליצור מחלקת Util ולהצהיר על הפונקציונליות הזו כפונקציה סטטית. ב-Cotlin אפשר להצהיר על פונקציות ברמה עליונה בלי כיתה. עם זאת, Kotlin מאפשר גם ליצור פונקציות של תוספים. פונקציות אלה מרחיבות סוג מסוים אך מוצהרות מחוץ לסוג זה. לכן, יש להם תחום עניין משותף מהסוג הזה.

ניתן להגביל את הרשאות הגישה של פונקציות ומאפיינים של תוספים באמצעות מגבילי חשיפה. הן מגבילות את השימוש בכיתות שצריכות את התוספים, והן לא מזהמות את מרחב השמות.

עבור המחלקה User, אנחנו יכולים להוסיף פונקציית תוסף שמחשב את השם בפורמט, או לשמור את השם המעוצב בנכס של תוסף. אפשר להוסיף אותו מחוץ לכיתה ב-Repository באותו קובץ:

// 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

לאחר מכן נוכל להשתמש בפונקציות ובמאפיינים של התוספים כאילו הם חלק מהכיתה User.

מכיוון שהשם בפורמט הוא נכס של המשתמש ולא פונקציונליות של המחלקה Repository, יש להשתמש בנכס התוסף. הקובץ Repository נראה עכשיו:

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

הספרייה הרגילה של Kotlin משתמשת בפונקציות של תוספים כדי להרחיב את הפונקציונליות של מספר ממשקי API של Java. פונקציות רבות ב-Iterable וב-Collection מיושמות כפונקציות של תוספים. לדוגמה, הפונקציה map שבה השתמשנו בשלב הקודם היא פונקציית תוסף ב-Iterable.

בקוד הכיתה Repository, אנחנו מוסיפים מספר אובייקטים של משתמשים לרשימה _users. ניתן לבצע את השיחות האלה בצורה אידאומטית יותר באמצעות פונקציות של היקף.

כדי להפעיל קוד רק בהקשר של אובייקט ספציפי, ללא צורך בגישה לאובייקט על סמך השם שלו, Kotlin יצר 5 פונקציות היקף: let, apply, with, run ו-also. הקצרים והחזקים, בכל הפונקציות האלה, יש מקלט (this), שעשוי להיות ארגומנט (it) והם יכולים להחזיר ערך. עליך להחליט באיזה מהם להשתמש, בהתאם למטרה שלך.

הנה דף מידע שימושי שיעזור לך לזכור את הדברים הבאים:

מכיוון שאנחנו מגדירים את האובייקט _users שלנו בRepository, אנחנו יכולים להפוך את הקוד לאידיומטי יותר באמצעות הפונקציה 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)
    }
 }

במעבדה זו התייחסנו ליסודות הנחוצים כדי להתחיל לקחת בחשבון מחדש את הקוד שלכם מ-Java ועד קוטלין. החישוב מחדש אינו תלוי בפלטפורמת הפיתוח ועוזר לכם להבטיח שהקוד שאתם כותבים הוא אידיומטי.

אידיומטי קוטלין יוצר קוד קצר ומתוק. עם כל התכונות שקוטלין מספקת, יש כל כך הרבה דרכים להפוך את הקוד שלכם לבטוח, תמציתי יותר וקריא יותר. לדוגמה, אנחנו יכולים אפילו לבצע אופטימיזציה של המחלקה שלנו ב-Repository על ידי יצירת רשימה _users של המשתמשים ישירות בהצהרה, וכך מבטלים את החסימה של init:

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

כללנו מגוון רחב של נושאים, החל מטיפול באפסים, טונים, מחרוזות ואוספים ועד נושאים כמו פונקציות תוספים, פונקציות ברמה עליונה, נכסים ופונקציות היקף. עברנו משתי כיתות Java ושני קורסים של Kotlin שנראים עכשיו כך:

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

להלן TL ו-DR של הפונקציות של Java והמיפוי שלהן ל-Kotlin:

Java

קוטלין

אובייקט אחד (final)

אובייקט אחד (val)

equals()

==

==

===

מחלקה ששומרת רק נתונים

כיתה אחת (data)

אתחול בבונה

אתחול בבלוק init

static שדות ופונקציות

השדות והפונקציות שהוכרזו ב-companion object

סינגל טון

object

כדי לקבל מידע נוסף על קוטלין ועל אופן השימוש בו בפלטפורמה שלכם, אפשר לעיין במקורות המידע הבאים: