1. שלום,
בקודלאב הזה תלמדו איך להמיר את הקוד מ-Java ל-Kotlin. בנוסף, נסביר מהן המוסכמות של שפת Kotlin ואיך לוודא שהקוד שאתם כותבים עומד בהן.
סדנת הקוד הזו מתאימה לכל מפתח שמשתמש ב-Java ושוקל להעביר את הפרויקט שלו ל-Kotlin. נתחיל עם כמה כיתות Java שתהפכו ל-Kotlin באמצעות סביבת הפיתוח המשולבת. לאחר מכן נבחן את הקוד המומר ונראה איך אפשר לשפר אותו על ידי הפיכתו לסגנוני יותר ולהימנע ממלכודות נפוצות.
מה תלמדו
תלמדו איך להמיר קוד Java ל-Kotlin. במהלך העבודה תלמדו את התכונות והמושגים הבאים בשפת Kotlin:
- טיפול באפשרות של ערך ריק
- הטמעת אובייקטים ייחודיים
- סיווגים של נתונים
- טיפול במחרוזות
- אופרטור Elvis
- ניתוק המבנה
- נכסים ונכסי תמיכה
- ארגומנטים שמוגדרים כברירת מחדל ופרמטרים עם שם
- עבודה עם קולקציות
- פונקציות של תוספים
- פונקציות ופרמטרים ברמה העליונה
- מילות המפתח
let
,apply
,with
ו-run
הנחות
צריך להיות לכם כבר ניסיון ב-Java.
מה נדרש
2. תהליך ההגדרה
יצירת פרויקט חדש
אם אתם משתמשים ב-IntelliJ IDEA, יוצרים פרויקט Java חדש עם Kotlin/JVM.
אם אתם משתמשים ב-Android Studio, יוצרים פרויקט חדש באמצעות התבנית No Activity. בוחרים ב-Kotlin כשפת הפרויקט. הערך של SDK המינימלי יכול להיות כל ערך, הוא לא ישפיע על התוצאה.
הקוד
נוצר אובייקט מודל מסוג User
וכיתה מסוג singleton Repository
שפועלת עם אובייקטים מסוג User
ומציגה רשימות של משתמשים ושמות משתמשים בפורמט.
יוצרים קובץ חדש בשם User.java
בקטע app/java/<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;
}
}
תראו שה-IDE מודיע שהמשתנה @Nullable
לא מוגדר. לכן, אם אתם משתמשים ב-Android Studio, עליכם לייבא את androidx.annotation.Nullable
, ואם אתם משתמשים ב-IntelliJ, עליכם לייבא את 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;
}
}
3. הצהרת תכונות nullability, val, var ו-data class
סביבת הפיתוח המשולבת שלנו יכולה לבצע המרה אוטומטית של קוד Java לקוד Kotlin בצורה טובה למדי, אבל לפעמים היא זקוקה לעזרה קטנה. נאפשר לסביבת הפיתוח המשולבת לבצע סבב ראשוני של המרה. לאחר מכן נעבור על הקוד שנוצר כדי להבין איך ומדוע הוא הומר כך.
עוברים לקובץ User.java
וממירים אותו ל-Kotlin: סרגל התפריטים -> קוד -> המרת קובץ Java לקובץ Kotlin.
אם מוצגת בקשה לתיקון אחרי ההמרה בסביבת הפיתוח המשולבת, לוחצים על כן.
הקוד ב-Kotlin אמור להיראות כך:
class User(var firstName: String?, var lastName: String?)
הערה: השם של User.java
השתנה ל-User.kt
. קובצי Kotlin כוללים את הסיומת .kt.
בכיתה User
ב-Java היו לנו שני מאפיינים: firstName
ו-lastName
. לכל אחד מהם הייתה שיטה לקבלת ערך (getter) ולקביעת ערך (setter), כך שהערך שלו היה ניתן לשינוי. מילת המפתח של Kotlin למשתנים שניתן לשינוי היא var
, ולכן הממיר משתמש ב-var
לכל אחד מהנכסים האלה. אם לנכסי ה-Java שלנו היו רק פונקציות getter, הם היו זמינים לקריאה בלבד והיו מוצהרים כמשתני val
. val
דומה למילת המפתח final
ב-Java.
אחד ההבדלים העיקריים בין Kotlin ל-Java הוא שב-Kotlin מציינים במפורש אם משתנה יכול לקבל ערך null. כדי לעשות זאת, המערכת מצרפת את הערך ?
להצהרת הסוג.
מכיוון שסימנו את firstName
ואת lastName
כאפשרויות nullable, הממיר האוטומטי סימן את המאפיינים כאפשרויות nullable באמצעות String?
. אם תוסיפו הערות למשתני Java כערכים שאינם null (באמצעות org.jetbrains.annotations.NotNull
או androidx.annotation.NonNull
), הממיר יזהה זאת ויגדיר את השדות כערכים שאינם null גם ב-Kotlin.
ההמרה הבסיסית כבר בוצעה. אבל אפשר לכתוב את זה בצורה רגילה יותר. בואו נראה איך עושים את זה.
סיווג נתונים
הכיתה User
שלנו מכילה רק נתונים. ב-Kotlin יש מילת מפתח לכיתות עם התפקיד הזה: data
. סימון הכיתה הזו ככיתה מסוג data
יגרום למהדר ליצור עבורנו באופן אוטומטי פונקציות getter ו-setter. הוא גם יניב את הפונקציות equals()
, hashCode()
ו-toString()
.
נוסיף את מילת המפתח data
לכיתה User
:
data class User(var firstName: String?, var lastName: String?)
ב-Kotlin, כמו ב-Java, יכול להיות קונסטרוקטור ראשי וקונסטרוקטור משני אחד או יותר. המבנה בדוגמה שלמעלה הוא המבנה הראשי של הכיתה User
. אם ממירים כיתה ב-Java שיש לה כמה קונסטרוקטורים, הממיר ייצור באופן אוטומטי כמה קונסטרוקטורים גם ב-Kotlin. הם מוגדרים באמצעות מילת המפתח constructor
.
אם רוצים ליצור מופע של הכיתה הזו, אפשר לעשות זאת כך:
val user1 = User("Jane", "Doe")
שוויון
ב-Kotlin יש שני סוגים של שוויון:
- כדי לבדוק אם שני מופעים זהים, משתמשים באופרטור
==
ובקריאה ל-equals()
. - בשוויון בין הפניות נעשה שימוש באופרטור
===
, ובודקים אם שתי הפניות מפנות לאותו אובייקט.
המאפיינים שמוגדרים ב-constructor הראשי של סוג הנתונים ישמשו לבדיקות של שוויון מבני.
val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false
4. ארגומנטים שמוגדרים כברירת מחדל, ארגומנטים עם שם
ב-Kotlin אפשר להקצות ערכים שמוגדרים כברירת מחדל לארגומנטים בקריאות לפונקציות. המערכת משתמשת בערך ברירת המחדל כשהארגומנט מושמט. ב-Kotlin, קונסטרוקטורים הם גם פונקציות, כך שאפשר להשתמש בארגומנטים שמוגדרים כברירת מחדל כדי לציין שערך ברירת המחדל של 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("Joe", "Doe")
ב-Kotlin אפשר לתייג את הארגומנטים כשקוראים לפונקציות:
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")
ערכי ברירת מחדל הם מושג חשוב שנעשה בו שימוש לעיתים קרובות בקוד Kotlin. בקודלאב שלנו אנחנו רוצים תמיד לציין את השם הפרטי ואת שם המשפחה בהצהרה על אובייקט User
, כך שלא נצטרך ערכים שמוגדרים כברירת מחדל.
5. התחלת אובייקטים, אובייקט נלווה ואובייקטים חד-משמעיים
לפני שממשיכים בקודלאב, צריך לוודא שהקלאס User
הוא קלאס data
. עכשיו נמיר את הכיתה Repository
ל-Kotlin. תוצאת ההמרה האוטומטית אמורה להיראות כך:
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)
}
}
בואו נראה מה הממיר האוטומטי עשה:
- הרשימה של
users
היא nullable כי האובייקט לא נוצר בזמן ההצהרה - פונקציות ב-Kotlin כמו
getUsers()
מוצהרות באמצעות המאפייןfun
- השיטה
getFormattedUserNames()
היא עכשיו נכס שנקראformattedUserNames
- לחזרה על רשימת המשתמשים (שהייתה בהתחלה חלק מ-
getFormattedUserNames(
) יש תחביר שונה מזה של Java - השדה
static
הוא עכשיו חלק מבלוקcompanion object
- נוסף בלוק
init
לפני שנמשיך, ננסה לנקות קצת את הקוד. אם נסתכל ב-constructor, נראה שהממיר הפך את הרשימה users
לרשימה שניתן לשינוי שמכילה אובייקטים שאפשר להקצות להם ערך null. אמנם הרשימה יכולה להיות null, אבל נניח שהיא לא יכולה להכיל משתמשים null. אז בואו נבצע את הפעולות הבאות:
- מסירים את
?
ב-User?
בהצהרת הסוגusers
- מסירים את
?
ב-User?
עבור סוג ההחזרה שלgetUsers()
כך שיוחזרList<User>?
בלוק 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
המאפיינים והשיטות של static
ב-Kotlin
ב-Java, אנחנו משתמשים במילות המפתח static
לשדות או לפונקציות כדי לציין שהם שייכים לכיתה, אבל לא למופעים של הכיתה. לכן יצרנו את השדה הסטטי INSTANCE
בכיתה Repository
. המקבילה לכך ב-Kotlin היא הבלוק companion object
. כאן גם מגדירים את השדות הסטטיים והפונקציות הסטטיות. הממיר יצר את הבלוק של אובייקט התווית הנלווית והעביר את השדה INSTANCE
לכאן.
טיפול באובייקטים ייחודיים (singletons)
מכיוון שאנחנו צריכים רק מופע אחד של המחלקה Repository
, השתמשנו בתבנית הסינגלון ב-Java. ב-Kotlin, אפשר לאכוף את התבנית הזו ברמת המהדר באמצעות החלפת מילת המפתח class
ב-object
.
מסירים את ה-constructor הפרטי ומחליפים את הגדרת הכיתה ב-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 formattedUserNames = Repository.formattedUserNames
הערה: אם לא מוגדר בנכס משתנה של רמת חשיפה, הוא ציבורי כברירת מחדל, כמו במקרה של הנכס formattedUserNames
באובייקט Repository
.
6. טיפול באפשרות של ערך ריק
במהלך ההמרה של הכיתה Repository
ל-Kotlin, הממיר האוטומטי הפך את רשימת המשתמשים ל-nullable, כי היא לא הוגדרה כאובייקט כשהצהירו עליה. כתוצאה מכך, בכל השימושים באובייקט users
, צריך להשתמש באופרטור !!
של טענת הנכוֹנוּת (assertion) של 'לא null'. (האותיות users!!
ו-user!!
יופיעו בקוד המומר). האופרטור !!
ממיר כל משתנה לסוג שאינו null, כך שתוכלו לגשת לנכסים או לבצע קריאה לפונקציות באמצעותו. עם זאת, תופיע חריגה אם הערך של המשתנה אכן יהיה null. השימוש ב-!!
עלול לגרום להשלכת חריגות בזמן הריצה.
במקום זאת, מומלץ לטפל באפשרות של ערך null באחת מהשיטות הבאות:
- ביצוע בדיקת null (
if (users != null) {...}
) - שימוש באופרטור elvis
?:
(הסבר על האופרטור מופיע בהמשך ה-Codelab) - שימוש בחלק מהפונקציות הסטנדרטיות של Kotlin (הנושא הזה ייבדק בהמשך הסדנה)
במקרה שלנו, אנחנו יודעים שלרשימת המשתמשים אין צורך להיות nullable, כי היא מאותחלת מיד אחרי יצירת האובייקט (בבלוק init
). כך אפשר ליצור ישירות מופע של האובייקט users
כשמגדירים אותו.
כשאתם יוצרים מופעים של סוגי אוספים, Kotlin מספקת כמה פונקציות עזר שיעזרו לכם לכתוב קוד קריא וגמיש יותר. כאן אנחנו משתמשים ב-MutableList
עבור users
:
private var users: MutableList<User>? = null
כדי לפשט את העניין, אפשר להשתמש בפונקציה mutableListOf()
ולציין את סוג רכיב הרשימה. mutableListOf<User>()
יוצר רשימה ריקה שיכולה להכיל אובייקטים מסוג User
. מאחר שהמידע על סוג הנתונים של המשתנה יכול להיכלל עכשיו על ידי המהדר, מסירים את הצהרת הסוג המפורשת של המאפיין users
.
private val users = mutableListOf<User>()
שינינו גם את var
ל-val
כי המשתמשים יכללו הפניה לקריאה בלבד לרשימת המשתמשים. חשוב לזכור שההפניה היא לקריאה בלבד, ולכן היא אף פעם לא יכולה להצביע על רשימה חדשה, אבל הרשימה עצמה עדיין ניתנת לשינוי (אפשר להוסיף או להסיר אלמנטים).
מאחר שמשתנה users
כבר מופעל, מסירים את ההפעלה הזו מהבלוק init
:
users = ArrayList<Any?>()
לאחר מכן, הבלוק 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)
}
בעקבות השינויים האלה, המאפיין users
הוא עכשיו לא null, ואנחנו יכולים להסיר את כל המופעים הלא נחוצים של אופרטור !!
. שימו לב שעדיין יופיעו שגיאות הידור ב-Android Studio, אבל עליכם להמשיך בשלבים הבאים של הקוד למעבדות כדי לפתור אותן.
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)
}
כמו כן, לגבי הערך userNames
, אם מציינים את הסוג של ArrayList
כמכיל את Strings
, אפשר להסיר את הסוג המפורש בהצהרה כי הוא יוסק.
val userNames = ArrayList<String>(users.size)
פירוק מבנה
ב-Kotlin אפשר לפרק אובייקט למספר משתנים באמצעות תחביר שנקרא הצהרת פירוק מבנה. אנחנו יוצרים כמה משתנים ויכולים להשתמש בהם בנפרד.
לדוגמה, כיתות data
תומכות בפירוק מבנה, כך שאפשר לפרק את האובייקט User
בלולאה for
ל-(firstName, lastName)
. כך אנחנו יכולים לעבוד ישירות עם הערכים firstName
ו-lastName
. מעדכנים את הלולאה for
כפי שמוצג בהמשך. מחליפים את כל המופעים של user.firstName
ב-firstName
ומחליפים את user.lastName
ב-lastName
.
for ((firstName, lastName) in users) {
var name: String
name = if (lastName != null) {
if (firstName != null) {
firstName + " " + lastName
} else {
lastName
}
} else if (firstName != null) {
firstName
} else {
"Unknown"
}
userNames.add(name)
}
if expression
השמות ברשימת userNames עדיין לא בפורמט הרצוי. מכיוון שגם lastName
וגם firstName
יכולים להיות null
, אנחנו צריכים לטפל באפשרות של ערך null כשאנחנו יוצרים את הרשימה של שמות המשתמשים בפורמט. אנחנו רוצים להציג את הערך "Unknown"
אם אחד מהשמות חסר. מאחר שמשתנה name
לא ישתנה אחרי שיוגדר פעם אחת, אפשר להשתמש ב-val
במקום ב-var
. קודם צריך לבצע את השינוי הזה.
val name: String
בודקים את הקוד שמגדיר את משתנה השם. יכול להיות שזה נראה לכם חדש: משתנה שמוגדר להיות שווה לבלוק קוד של if
או else
. מותר לעשות זאת כי ב-Kotlin, if
ו-when
הם ביטויים – הם מחזירים ערך. השורה האחרונה של משפט if
תוקצה ל-name
. המטרה היחידה של הבלוק הזה היא לאתחל את הערך של name
.
בעיקרון, הלוגיקה הזו שמוצגת כאן היא שאם הערך של lastName
הוא null, הערך של name
מוגדר ל-firstName
או ל-"Unknown"
.
name = if (lastName != null) {
if (firstName != null) {
firstName + " " + lastName
} else {
lastName
}
} else if (firstName != null) {
firstName
} else {
"Unknown"
}
אופרטור Elvis
אפשר לכתוב את הקוד הזה בצורה רגילה יותר באמצעות אופרטור ה-elvis ?:
. אופרטור ה-elvis יחזיר את הביטוי בצד ימין אם הוא לא null, או את הביטוי בצד ימין אם הצד ימין הוא null.
לכן בקוד הבא, הערך firstName
מוחזר אם הוא לא null. אם הערך של firstName
הוא null, הביטוי מחזיר את הערך שמשמאל , "Unknown"
:
name = if (lastName != null) {
...
} else {
firstName ?: "Unknown"
}
7. תבניות מחרוזות
ב-Kotlin קל לעבוד עם String
s באמצעות תבניות מחרוזות. תבניות מחרוזות מאפשרות להפנות למשתנים בתוך הצהרות על מחרוזות באמצעות הסמל $ לפני המשתנה. אפשר גם להוסיף ביטוי להצהרה על מחרוזת. לשם כך, מניחים את הביטוי בתוך { } ומשתמשים בסמל $ לפניו. דוגמה: ${user.firstName}
.
הקוד שלך משתמש כרגע בשרשור מחרוזות כדי לשלב את firstName
ו-lastName
בשם המשתמש.
if (firstName != null) {
firstName + " " + lastName
}
במקום זאת, מחליפים את שרשור המחרוזות ב:
if (firstName != null) {
"$firstName $lastName"
}
שימוש בתבניות מחרוזות יכול לפשט את הקוד.
אם יש דרך רגילה יותר לכתוב את הקוד, יוצגו אזהרות בסביבת הפיתוח המשולבת. יופיע קו תחתון מקווקו בקוד, וכשתעבירו את העכבר מעליו תוצג הצעה לשיפור הקוד.
בשלב הזה, אמורה להופיע אזהרה על כך שאפשר לצרף את ההצהרה name
למטלה. ניישם את זה. מכיוון שאפשר להסיק את הסוג של המשתנה name
, אפשר להסיר את ההצהרה המפורשת על סוג String
. עכשיו 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
}
אנחנו יכולים לבצע עוד שינוי קטן. הלוגיקה של ממשק המשתמש שלנו מציגה את הערך "Unknown"
במקרה שהשם הפרטי ושם המשפחה חסרים, לכן אנחנו לא תומכים באובייקטים null. לכן, עבור סוג הנתונים formattedUserNames
, מחליפים את List<String?>
ב-List<String>
.
val formattedUserNames: List<String>
8. פעולות על קולקציות
נבחן מקרוב את ה-getter של 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
}
}
שימו לב שאנחנו משתמשים באופרטור Elvis כדי להחזיר את הערך "Unknown"
אם הערך של user.lastName
הוא null, כי user.lastName
הוא מסוג String?
ונדרש String
ל-name
.
...
else {
user.lastName ?: "Unknown"
}
...
כדי לפשט את העניין עוד יותר, אפשר להסיר את המשתנה 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"
}
}
}
9. נכסים ונכסי תמיכה
ראינו שהממיר האוטומטי החליף את הפונקציה getFormattedUserNames()
במאפיין שנקרא formattedUserNames
עם פונקציית getter בהתאמה אישית. מתחת לפני השטח, Kotlin עדיין יוצרת שיטה getFormattedUserNames()
שמחזירה List
.
ב-Java, אנחנו חושפים את מאפייני הכיתה באמצעות פונקציות getter ו-setter. ב-Kotlin אפשר להבחין טוב יותר בין מאפיינים של כיתה, שמתבטאים בשדות, לבין פונקציונליות, פעולות שכייתה יכולה לבצע, שמתבטאות בפונקציות. במקרה שלנו, הכיתה Repository
פשוטה מאוד ולא מבצעת פעולות, ולכן יש בה רק שדות.
הלוגיקה שהופעל בפונקציה getFormattedUserNames()
של Java מופעלת עכשיו כשקוראים ל-getter של מאפיין formattedUserNames
ב-Kotlin.
אין לנו שדה מפורש שתואם למאפיין formattedUserNames
, אבל Kotlin מספקת לנו שדה תמיכה אוטומטי בשם field
, שאנחנו יכולים לגשת אליו במקרה הצורך באמצעות פונקציות getter ו-setter בהתאמה אישית.
עם זאת, לפעמים אנחנו רוצים פונקציונליות נוספת שלא קיימת בשדה הנגזר האוטומטי.
נסביר את זה בעזרת דוגמה.
בכיתה Repository
יש לנו רשימה של משתמשים שניתנת לשינוי, שחשופה בפונקציה getUsers()
שנוצרה מקוד ה-Java שלנו:
fun getUsers(): List<User>? {
return users
}
לא רצינו שמבצעי הקריאה של הכיתה Repository
ישנו את רשימת המשתמשים, לכן יצרנו את הפונקציה getUsers()
שמחזירה List<User>
לקריאה בלבד. במקרים כאלה, אנחנו מעדיפים להשתמש בנכסים במקום בפונקציות ב-Kotlin. באופן מדויק יותר, נחשוף List<User>
לקריאה בלבד שמגובת על ידי mutableListOf<User>
.
קודם כול, נשנה את השם של users
ל-_users
. מדגישים את שם המשתנה ולוחצים לחיצה ימנית כדי לשנות את הקוד > לשנות שם של המשתנה. לאחר מכן מוסיפים נכס ציבורי לקריאה בלבד שמחזיר רשימה של משתמשים. ניקרא לו users
:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
בשלב הזה אפשר למחוק את השיטה getUsers()
.
בעקבות השינוי שלמעלה, הנכס הפרטי _users
הופך לנכס התומך של הנכס הציבורי users
. מחוץ לכיתה Repository
, אי אפשר לשנות את הרשימה _users
, כי לצרכני הכיתה יש גישה לרשימה רק דרך users
.
כשקוראים ל-users
מקוד Kotlin, נעשה שימוש בהטמעה של List
מ-Kotlin Standard Library, שבה אי אפשר לשנות את הרשימה. אם users
נקרא מ-Java, נעשה שימוש בהטמעה של java.util.List
, שבה אפשר לשנות את הרשימה וזמינות פעולות כמו add() ו-remove().
הקוד המלא:
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)
}
}
10. פונקציות ונכסים ברמה העליונה ובתוספים
בשלב הזה, הכיתה Repository
יודעת לחשב את שם המשתמש בפורמט של אובייקט User
. אבל אם רוצים לעשות שימוש חוזר באותה לוגיקה של עיצוב בכיתות אחרות, צריך להעתיק ולהדביק אותה או להעביר אותה לכיתה User
.
ב-Kotlin אפשר להצהיר על פונקציות ומאפיינים מחוץ לכל כיתה, אובייקט או ממשק. לדוגמה, הפונקציה mutableListOf()
שבה השתמשנו כדי ליצור מכונה חדשה של List
כבר מוגדרת ב-Collections.kt
מ-Kotlin Standard Library.
ב-Java, בכל פעם שצריך פונקציונליות של כלי שימושי, סביר להניח שתיצרו את הכיתה Util
ותצהירו על הפונקציונליות הזו כפונקציה סטטית. ב-Kotlin אפשר להצהיר על פונקציות ברמה העליונה בלי שיהיה צורך בכיתה. עם זאת, ב-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
.
מכיוון שהשם המעוצב הוא מאפיין של הכיתה 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 Standard Library נעשה שימוש בפונקציות הרחבה כדי להרחיב את הפונקציונליות של כמה ממשקי API של Java. פונקציות רבות ב-Iterable
וב-Collection
מיושמות כפונקציות הרחבה. לדוגמה, הפונקציה map
שבה השתמשנו בשלב קודם היא פונקציית תוסף ב-Iterable
.
11. פונקציות היקף: let, apply, with, run, also
בקוד הכיתה Repository
, מוסיפים כמה אובייקטים מסוג User
לרשימה _users
. אפשר לשפר את הקריאות האלה באמצעות פונקציות היקף של Kotlin.
כדי להריץ קוד רק בהקשר של אובייקט ספציפי, בלי צורך לגשת לאובייקט על סמך השם שלו, ב-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)
}
}
12. סיכום
בסדנת הקוד הזו, סקרנו את היסודות שצריך כדי להתחיל להמיר את הקוד מ-Java ל-Kotlin. ההמרה הזו לא תלויה בפלטפורמת הפיתוח שלכם, והיא עוזרת לוודא שהקוד שאתם כותבים הוא ב-Kotlin.
כשמשתמשים ב-Kotlin באופן שתואם לשפה, אפשר לכתוב קוד קצר ופשוט. בעזרת כל התכונות של Kotlin, יש הרבה דרכים לשפר את הבטיחות, הקוהרנטיות והקריאוּת של הקוד. לדוגמה, אפשר גם לבצע אופטימיזציה של הכיתה Repository
על ידי יצירה של רשימת _users
עם משתמשים ישירות בהצהרה, וכך להיפטר מהבלוק init
:
private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
התייחסנו למגוון רחב של נושאים, החל מהטיפול באפשרות של ערך null, מונולידים, מחרוזות ואוספים, ועד לנושאים כמו פונקציות תוסף, פונקציות ברמה העליונה, מאפיינים ופונקציות היקף. עברנו משתי כיתות Java לשתי כיתות Kotlin שנראות עכשיו כך:
User.kt
data 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 }
}
ריכזנו כאן את הפונקציות של Java ואת המיפוי שלהן ל-Kotlin:
Java | Kotlin |
אובייקט | אובייקט |
|
|
|
|
כיתה שמכילה רק נתונים | הכיתה |
אתחול ב-constructor | אתחול בבלוק |
| שדות ופונקציות שהוגדרו ב- |
כיתה מסוג Singleton |
|
למידע נוסף על Kotlin ועל האופן שבו משתמשים בה בפלטפורמה, אפשר לעיין במקורות המידע הבאים:
- Kotlin Koans
- מדריכים בנושא Kotlin
- Android Kotlin Fundamentals
- Kotlin Bootcamp למתכנתים
- Kotlin למפתחי Java – קורס חינמי במצב ביקורת