שימוש בדפוסי Kotlin נפוצים ב-Android

נושא זה מתמקד בכמה מההיבטים השימושיים ביותר של שפת Kotlin במהלך הפיתוח ל-Android.

עבודה עם מקטעים

בקטעים הבאים נעשה שימוש ב-Fragment דוגמאות כדי להדגיש חלק את התכונות הכי טובות.

ירושה

אפשר להצהיר על כיתה ב-Kotlin באמצעות מילת המפתח class. בתוך לדוגמה, LoginFragment הוא מחלקה משנית של Fragment. אפשר לציין ירושה באמצעות האופרטור : בין מחלקה משנית לבין ההורה שלה:

class LoginFragment : Fragment()

בהצהרה לכיתה הזו, באחריות LoginFragment לבצע קריאה ל של מחלקת העל שלו, Fragment.

תוך LoginFragment, ניתן יהיה לבטל מספר קריאות חוזרות (callback) של מחזור החיים כדי תגובה לשינויים במצב בFragment. כדי לשנות פונקציה, משתמשים בפונקציה מילת מפתח אחת (override), כמו בדוגמה הבאה:

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}

כדי להפנות לפונקציה במחלקה ההורה, יש להשתמש במילת המפתח super, כפי שמוצג בדוגמה הבאה:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}

איפוס ואתחול

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

ב-Kotlin צריך לאתחל את מאפייני (properties) של אובייקט כשמצהירים על האובייקט. המשמעות היא שכשמקבלים מופע של כיתה, אפשר מיד להתייחס לכל אחד מהמאפיינים הנגישים שלו. האובייקטים View ב-Fragment, עם זאת, לא מוכנים לניפוח עד שקוראים ל-Fragment#onCreateView, לכן צריכה להיות לך דרך לדחות אתחול נכס עבור View.

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

הדוגמה הבאה ממחישה את השימוש ב-lateinit כדי להקצות View אובייקטים ב onViewCreated:

class LoginFragment : Fragment() {

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }

    ...
}

המרה של SAM

כדי להאזין לאירועי קליק ב-Android, אפשר להטמיע את ממשק OnClickListener. Button אובייקטים מכילים setOnClickListener() שלוקחת את ההטמעה של OnClickListener.

ב-OnClickListener יש שיטה מופשטת אחת, onClick(), שצריך להטמיע. כי setOnClickListener() תמיד לוקחת OnClickListener בתור ארגומנט, ובגלל של-OnClickListener יש תמיד אותו מופשט יחיד את ההטמעה הזו אפשר לייצג באמצעות פונקציה אנונימית Kotlin. התהליך הזה נקרא Single Avestract Method conversion (המרה אחת בשיטה מופשטת יחידה), או המרת SAM.

המרת SAM יכולה להפוך את הקוד שלכם לנקי יותר באופן משמעותי. הדוגמה הבאה מראה איך להשתמש בהמרה של SAM כדי להטמיע OnClickListener Button:

loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}

הקוד בפונקציה האנונימית שמועבר אל setOnClickListener() מופעל כשמשתמש לוחץ על loginButton.

אובייקטים נלווים

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

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

class LoginFragment : Fragment() {

    ...

    companion object {
        private const val TAG = "LoginFragment"
    }
}

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

הענקת גישה לנכסים

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

private val viewModel: LoginViewModel by viewModels()

'הענקת גישה לנכסים' מספקת הטמעה משותפת שניתן לעשות בה שימוש חוזר בכל רחבי האפליקציה. מערכת Android KTX מספקת לכם הרשאות גישה מסוימות לנכס. לדוגמה, viewModels, מאחזרת ViewModel בהיקף של Fragment הנוכחי.

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

ערך אפס

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

לדוגמה, הביטוי הבא לא חוקי ב-Kotlin. name הוא מסוג String ואי אפשר לבטל אותו:

val name: String = null

כדי לאפשר ערך null, עליך להשתמש בסוג String (ללא null), String?, כמו שמוצגת בדוגמה הבאה:

val name: String? = null

יכולת פעולה הדדית

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

לעיתים קרובות צריך גם לבצע קריאה לקוד שאינו קוטלין כשכותבים אפליקציה ל-Android, רוב ממשקי ה-API של Android נכתבים בשפת Java.

הערך של Nullability הוא תחום מרכזי שבו ההתנהגות של Java ו-Kotlin שונים. Java היא פחות מחמיר עם תחביר של יכולת null.

לדוגמה, למחלקה Account יש כמה מאפיינים, כולל String לנכס שנקרא name. ל-Java אין את כללי Kotlin בנוגע ליכולת null, במקום להסתמך על הערות אופציונליות בנושא יכולת null כדי להצהיר במפורש האם ניתן להקצות ערך null.

מאחר ש-framework של Android כתוב בעיקר ב-Java, ייתכן שתיתקלו במקרה של קריאה לממשקי API ללא הערות לגבי יכולת null.

סוגי פלטפורמות

אם משתמשים ב-Kotlin כדי להפנות לחבר name ללא הערה, שמוגדר מחלקה Account של Java, המהדר לא יודע אם ה-String ממופה String או String? ב-Kotlin. חוסר בהירות זה מיוצג באמצעות platform type, String!.

ל-String! אין משמעות מיוחדת עבור המהדר של Kotlin. String! יכול לייצג String או String?, והמהדר מאפשר להקצות את הערך מכל סוג. לתשומת ליבך, אתה עלול להשליך NullPointerException אם שמייצגים את הסוג בתור String ולהקצות ערך null.

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

לדוגמה, זוהי המחלקה Account כפי שהיא מוגדרת ב-Java:

public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;

    ...
}

לאחד ממשתני החברים, accessId, נוספו הערות עם @Nullable, שמציין שהוא יכול להכיל ערך null. לאחר מכן, Kotlin יטפל ב-accessId בתור String?.

כדי לציין שמשתנה מסוים לא יכול להיות null, משתמשים בהערה @NonNull:

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

בתרחיש הזה, name נחשב כ-String שאינו אפס (null) ב-Kotlin.

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

טיפול ב-null

אם אתם לא בטוחים לגבי סוג Java מסוים, צריך להתייחס אליו כאל null. לדוגמה, למשתמש name בכיתה Account אין הערות, ולכן צריך להניח שהוא String מאפשר ערך null.

אם רוצים לחתוך את name כך שהערך שלו לא יכלול את התחילית או את בסוף, אפשר להשתמש בפונקציה trim של Kotlin. אפשר לחתוך בבטחה String? בכמה דרכים שונות. אחת מהדרכים האלה היא להשתמש בפונקציה not-null האופרטור של טענת נכוֹנוּת (assertion), !!, כמו בדוגמה הבאה:

val account = Account("name", "type")
val accountName = account.name!!.trim()

האופרטור !! מתייחס לכל מה שמופיע בצד שמאל שלו כאל ערך אפס, במקרה הזה, את name מתייחסת בתור String שאינו null. אם התוצאה של משמאל לביטוי הזה הוא null, ואז האפליקציה יקפיץ NullPointerException. אופרטור זה הוא מהיר וקל, אבל כדאי להשתמש בו באופן מדוד, מכיוון שהוא להחזיר מופעים של NullPointerException לקוד.

עדיף להשתמש באופרטור השיחה הבטוחה, ?., כמו שמוצג בדוגמה הבאה:

val account = Account("name", "type")
val accountName = account.name?.trim()

באמצעות אופרטור Safe-call, אם הערך של name הוא לא null, אז התוצאה של name?.trim() הוא ערך שם ללא רווחים לבנים בתחילתו או בסופה. אם המיקום הערך name הוא null, אז התוצאה של name?.trim() היא null. המשמעות היא האפליקציה שלך אף פעם לא יכולה לזרוק NullPointerException במהלך ביצוע ההצהרה הזו.

אופרטור השיחה הבטוחה חוסך לך מNullPointerException אפשרי, היא כן מעבירה ערך null להצהרה הבאה. במקום זאת, אפשר לטפל ב-null במקרים מיידיים באמצעות אופרטור של Elvis (?:), כפי שמוצג בהסבר הבא דוגמה:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

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

אפשר גם להשתמש באופרטור Elvis כדי לחזור מפונקציה בשלב מוקדם, כמו שמוצג כאן בדוגמה הבאה:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

שינויים ב-Android API

ממשקי Android API הופכים יותר ויותר ידידותיים ל-Kotlin. הרבה ממכשירי Android ממשקי ה-API הנפוצים ביותר, כולל AppCompatActivity ו-Fragment, כוללים הערות לגבי ביטולי זמינות, ושיחות מסוימות כמו Fragment#getContext יותר ידידותיות ל-Kotlin.

לדוגמה, גישה אל Context של Fragment היא כמעט תמיד לא null, מכיוון שרוב השיחות שיבוצעו בFragment מתרחשות כאשר Fragment מצורף אל Activity (תת-מחלקה של Context). עם זאת, Fragment#getContext לא תמיד מחזירה ערך שאינו null, כי יש במצבים שבהם Fragment לא מצורף אל Activity. כלומר, ההחזרה הסוג של Fragment#getContext יכול להיות null.

מכיוון שהערך של Context שהוחזר מ-Fragment#getContext הוא null (והוא גם מופיע כ- @Nullable), עליכם להתייחס אליו בתור Context? בקוד Kotlin. כלומר, החלה של אחד מהאופרטורים שהוזכרו קודם לכן להיות null לפני גישה למאפיינים ולפונקציות שלו. לגבי חלק מאלה ב-Android יש ממשקי API חלופיים שמספקים את הנוחות הזו. לדוגמה, Fragment#requireContext, מחזירה Context שאינו null ומחזירה הפונקציה IllegalStateException מקבלת קריאה כשהערך של Context הוא null. כך, אפשר להתייחס לערך של Context שמתקבל כערך שאינו null ללא צורך מפעילי שיחה בטוחה או דרכים לעקוף את הבעיה.

אתחול נכס

מאפיינים ב-Kotlin לא מאותחלים כברירת מחדל. צריך לאתחל אותן כשהכיתוב המצורף מאותחל.

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

class LoginFragment : Fragment() {
    val index: Int = 12
}

ניתן להגדיר את האתחול הזה גם בבלוק מאתחל:

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

בדוגמאות שלמעלה, index מאותחל כאשר LoginFragment הוא שנוצרו.

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

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

class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}

הפעולה הזו עובדת כמו שצריך, אבל עכשיו צריך לנהל את יכולת ה-null של View בכל פעם שמתייחסים אליהם. פתרון מועיל יותר הוא להשתמש ב-lateinit בשביל View כמו בדוגמה הבאה:

class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}

מילת המפתח lateinit מאפשרת לכם להימנע מאתחול נכס כאשר של האובייקט. אם יש הפניה לנכס לפני האתחול, Kotlin זורקים UninitializedPropertyAccessException, אז כדאי לוודא לאתחל את הנכס שלך בהקדם האפשרי.