ใช้รูปแบบ Kotlin ทั่วไปกับ Android

หัวข้อนี้มุ่งเน้นที่แง่มุมที่มีประโยชน์ที่สุดบางอย่างของภาษา Kotlin เมื่อพัฒนาแอปสำหรับ Android

ทำงานกับ Fragment

ส่วนต่อไปนี้ใช้Fragmentตัวอย่างเพื่อเน้นฟีเจอร์ที่ดีที่สุดบางส่วนของ Kotlin

การรับช่วง

คุณประกาศคลาสใน Kotlin ได้ด้วยคีย์เวิร์ด class ในตัวอย่างต่อไปนี้ LoginFragment เป็นคลาสย่อยของ Fragment คุณระบุการรับค่าได้โดยใช้โอเปอเรเตอร์ : ระหว่างคลาสย่อยกับคลาสหลัก

class LoginFragment : Fragment()

ในการประกาศคลาสนี้ LoginFragment มีหน้าที่เรียก ตัวสร้างของคลาสระดับบน Fragment

ภายใน LoginFragment คุณสามารถลบล้างการเรียกกลับของวงจรหลายรายการเพื่อ ตอบสนองต่อการเปลี่ยนแปลงสถานะใน 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 อย่างปลอดภัย

ใน Kotlin คุณต้องเริ่มต้นพร็อพเพอร์ตี้ของออบเจ็กต์เมื่อประกาศออบเจ็กต์ ซึ่งหมายความว่าเมื่อคุณได้รับอินสแตนซ์ของคลาส คุณจะอ้างอิงพร็อพเพอร์ตี้ที่เข้าถึงได้ทันที อย่างไรก็ตาม ออบเจ็กต์ 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)
    }

    ...
}

Conversion ของ SAM

คุณสามารถรอเหตุการณ์คลิกใน Android ได้โดยการใช้ OnClickListener อินเทอร์เฟซ ออบเจ็กต์ Button มีฟังก์ชัน setOnClickListener() ที่ใช้การติดตั้งใช้งาน OnClickListener

OnClickListener มีเมธอดนามธรรมเดียวคือ onClick() ซึ่งคุณต้อง ใช้ เนื่องจาก setOnClickListener() จะรับ OnClickListener เป็นอาร์กิวเมนต์เสมอ และเนื่องจาก OnClickListener มีเมธอดแบบนามธรรมเดียวเหมือนกันเสมอ การใช้งานนี้จึงแสดงได้โดยใช้ฟังก์ชันที่ไม่ระบุชื่อใน Kotlin กระบวนการนี้เรียกว่า การแปลงตามรูปแบบ Single Abstract Method หรือการแปลง SAM

การแปลง SAM จะช่วยให้โค้ดของคุณสะอาดขึ้นอย่างมาก ตัวอย่างต่อไปนี้ แสดงวิธีใช้ Conversion ของ 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 ที่ระดับบนสุดของไฟล์ได้ แต่ไฟล์อาจมีตัวแปร ฟังก์ชัน และคลาสจำนวนมากที่กำหนดไว้ที่ระดับบนสุดด้วย ออบเจ็กต์คู่ช่วยเชื่อมต่อตัวแปร ฟังก์ชัน และคำจำกัดความของคลาสโดยไม่ต้องอ้างอิงอินสแตนซ์ใดๆ ของคลาสนั้น

การมอบสิทธิ์ของพร็อพเพอร์ตี้

เมื่อเริ่มต้นพร็อพเพอร์ตี้ คุณอาจทำซ้ำรูปแบบที่พบบ่อยกว่าของ Android บางรูปแบบ เช่น การเข้าถึง ViewModel ภายใน Fragment คุณสามารถใช้ไวยากรณ์การมอบสิทธิ์พร็อพเพอร์ตี้ของ Kotlin เพื่อหลีกเลี่ยงโค้ดที่ซ้ำกันมากเกินไปได้

private val viewModel: LoginViewModel by viewModels()

การมอบสิทธิ์พร็อพเพอร์ตี้จะมีการติดตั้งใช้งานทั่วไปที่คุณนำกลับมาใช้ใหม่ได้ ทั่วทั้งแอป Android KTX มีการมอบสิทธิ์พร็อพเพอร์ตี้บางอย่างให้คุณ เช่น viewModels จะดึงข้อมูล ViewModel ที่กำหนดขอบเขตไว้สำหรับ Fragment ปัจจุบัน

การมอบสิทธิ์พร็อพเพอร์ตี้ใช้การสะท้อน ซึ่งจะเพิ่มค่าใช้จ่ายด้านประสิทธิภาพบางอย่าง ข้อแลกเปลี่ยนคือไวยากรณ์ที่กระชับซึ่งช่วยประหยัดเวลาในการพัฒนา

ความสามารถในการเว้นว่าง

Kotlin มีกฎเกี่ยวกับ Nullability ที่เข้มงวดซึ่งรักษาความปลอดภัยของประเภทในแอปของคุณ โดยค่าเริ่มต้น การอ้างอิงไปยังออบเจ็กต์ใน Kotlin จะต้องไม่มีค่า Null หากต้องการกำหนดค่า Null ให้กับตัวแปร คุณต้องประกาศประเภทตัวแปรที่ยอมรับค่า Null โดยการเพิ่ม ? ที่ท้ายประเภทฐาน

ตัวอย่างเช่น นิพจน์ต่อไปนี้ไม่ถูกต้องใน Kotlin name มีประเภท String และไม่ใช่ค่าที่กำหนดให้เป็น Null ได้

val name: String = null

หากต้องการอนุญาตค่า Null คุณต้องใช้ประเภท String ที่อนุญาตให้เป็น Null ได้ String? ดังที่แสดงในตัวอย่างต่อไปนี้

val name: String? = null

ความสามารถในการทำงานร่วมกัน

กฎที่เข้มงวดของ Kotlin ทำให้โค้ดปลอดภัยและกระชับยิ่งขึ้น กฎเหล่านี้จะช่วยลดโอกาสที่จะเกิดNullPointerExceptionซึ่งอาจทำให้แอปของคุณ ขัดข้อง นอกจากนี้ ยังช่วยลดจำนวนการตรวจสอบค่า Null ที่คุณต้องทำในโค้ดด้วย

บ่อยครั้งที่คุณต้องเรียกใช้โค้ดที่ไม่ใช่ Kotlin เมื่อเขียนแอป Android เนื่องจาก API ของ Android ส่วนใหญ่เขียนด้วยภาษาโปรแกรม Java

Nullability เป็นส่วนสำคัญที่ Java และ Kotlin มีลักษณะการทำงานแตกต่างกัน Java มีความเข้มงวดน้อยกว่า ในเรื่องไวยากรณ์การกำหนดค่า Null

ตัวอย่างเช่น คลาส Account มีพร็อพเพอร์ตี้ 2-3 รายการ รวมถึงพร็อพเพอร์ตี้ String ที่ชื่อ name Java ไม่มีกฎของ Kotlin เกี่ยวกับค่า Null แต่จะใช้คำอธิบายประกอบเกี่ยวกับค่า Null ที่ไม่บังคับเพื่อประกาศอย่างชัดเจน ว่าคุณกำหนดค่า Null ได้หรือไม่

เนื่องจากเฟรมเวิร์ก Android เขียนด้วยภาษา Java เป็นหลัก คุณจึงอาจพบสถานการณ์นี้เมื่อเรียกใช้ API โดยไม่มีคำอธิบายประกอบเกี่ยวกับค่า Null

ประเภทแพลตฟอร์ม

หากคุณใช้ Kotlin เพื่ออ้างอิงสมาชิก name ที่ไม่ได้ใส่คำอธิบายประกอบซึ่งกำหนดไว้ในคลาส Account ของ Java คอมไพเลอร์จะไม่ทราบว่า String แมปกับ String หรือ String? ใน Kotlin ความคลุมเครือนี้แสดงผ่านประเภทแพลตฟอร์ม String!

String! ไม่มีความหมายพิเศษสำหรับคอมไพเลอร์ Kotlin String! สามารถแสดงถึง ทั้ง String หรือ String? และคอมไพเลอร์จะช่วยให้คุณกำหนดค่าของ ประเภทใดก็ได้ โปรดทราบว่าคุณอาจทำให้เกิด NullPointerException หากแสดงประเภทเป็น String และกำหนดค่าเป็น Null

หากต้องการแก้ไขปัญหานี้ คุณควรใช้คำอธิบายประกอบเกี่ยวกับค่า 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

คำอธิบายประกอบการยอมรับค่า Null จะรวมอยู่ใน Android API ใหม่ทั้งหมดและ Android API ที่มีอยู่หลายรายการ ไลบรารี Java หลายรายการได้เพิ่มคำอธิบายประกอบเกี่ยวกับค่า Null เพื่อให้รองรับทั้งนักพัฒนาซอฟต์แวร์ Kotlin และ Java ได้ดียิ่งขึ้น

การจัดการความสามารถในการเว้นว่าง

หากไม่แน่ใจเกี่ยวกับประเภท Java คุณควรพิจารณาว่าประเภทนั้นเป็นประเภทที่กำหนดให้เป็น Null ได้ ตัวอย่างเช่น ระบบไม่ได้ใส่คำอธิบายประกอบให้กับสมาชิก name ของคลาส Account ดังนั้นคุณ ควรสมมติว่าสมาชิกดังกล่าวเป็น String? ที่อนุญาตให้เป็นค่าว่างได้

หากต้องการตัด name เพื่อให้ค่าไม่มีช่องว่างนำหน้าหรือต่อท้าย คุณสามารถใช้ฟังก์ชัน trim ของ Kotlin ได้ คุณตัดString?ได้อย่างปลอดภัยด้วยวิธีต่างๆ วิธีหนึ่งคือการใช้ตัวดำเนินการยืนยันว่าไม่ใช่ค่า Null !! ดังที่แสดงในตัวอย่างต่อไปนี้

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

ตัวดำเนินการ !! จะถือว่าทุกอย่างทางด้านซ้ายมือเป็นค่าที่ไม่ใช่ Null ดังนั้นในกรณีนี้ คุณจะถือว่า name เป็น String ที่ไม่ใช่ Null หากผลลัพธ์ของ นิพจน์ทางด้านซ้ายเป็น Null แอปจะแสดง NullPointerException โอเปอเรเตอร์นี้ใช้งานง่ายและรวดเร็ว แต่ควรใช้อย่างระมัดระวังเนื่องจากอาจทำให้NullPointerExceptionกลับมาปรากฏในโค้ดอีกครั้ง

ตัวเลือกที่ปลอดภัยกว่าคือการใช้โอเปอเรเตอร์การเรียกที่ปลอดภัย ?. ดังที่แสดงใน ตัวอย่างต่อไปนี้

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

เมื่อใช้ตัวดำเนินการเรียกอย่างปลอดภัย หาก name ไม่ใช่ค่าว่าง ผลลัพธ์ของ name?.trim() จะเป็นค่าชื่อที่ไม่มีช่องว่างนำหน้าหรือต่อท้าย หาก name เป็น Null ผลลัพธ์ของ name?.trim() จะเป็น null ซึ่งหมายความว่าแอปของคุณจะไม่มีทางส่ง NullPointerException เมื่อดำเนินการกับคำสั่งนี้

แม้ว่าตัวดำเนินการเรียกอย่างปลอดภัยจะช่วยให้คุณไม่ต้องกังวลเรื่อง NullPointerException แต่ก็ส่งค่า Null ไปยังคำสั่งถัดไป คุณสามารถจัดการกรณีค่า Null ได้ทันทีโดยใช้ตัวดำเนินการ Elvis (?:) ดังที่แสดงในตัวอย่างต่อไปนี้

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

หากผลลัพธ์ของนิพจน์ทางด้านซ้ายของตัวดำเนินการ Elvis เป็น null ระบบจะกำหนดค่าทางด้านขวาให้กับ accountName เทคนิคนี้มีประโยชน์ในการระบุค่าเริ่มต้นที่มิฉะนั้นจะเป็นค่า Null

นอกจากนี้ คุณยังใช้ตัวดำเนินการ Elvis เพื่อออกจากฟังก์ชันก่อนเวลาอันควรได้ด้วย ดังที่แสดง ในตัวอย่างต่อไปนี้

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

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

    ...
}

การเปลี่ยนแปลง API ของ Android

Android API กำลังเป็นมิตรกับ Kotlin มากขึ้นเรื่อยๆ API ที่ใช้กันมากที่สุดของ Android หลายรายการ ซึ่งรวมถึง AppCompatActivity และ Fragment มี คำอธิบายประกอบเกี่ยวกับค่า Null และการเรียกใช้บางอย่าง เช่น Fragment#getContext มี ทางเลือกอื่นที่เหมาะกับ Kotlin มากกว่า

ตัวอย่างเช่น การเข้าถึง Context ของ Fragment มักจะไม่เป็นค่าว่าง เนื่องจากการเรียกส่วนใหญ่ที่คุณทำใน Fragment จะเกิดขึ้นขณะที่ Fragment แนบอยู่กับ Activity (คลาสย่อยของ Context) อย่างไรก็ตาม Fragment#getContext ไม่ได้แสดงค่าที่ไม่ใช่ค่าว่างเสมอไป เนื่องจากมี สถานการณ์ที่ Fragment ไม่ได้แนบอยู่กับ Activity ดังนั้น ประเภทการคืนค่าของ Fragment#getContext จึงเป็นค่าที่กำหนดให้เป็น Null ได้

เนื่องจาก Context ที่แสดงผลจาก Fragment#getContext เป็นค่าที่เว้นว่างได้ (และมี คำอธิบายประกอบเป็น @Nullable) คุณจึงต้องถือว่าเป็น Context? ในโค้ด Kotlin ซึ่งหมายถึงการใช้ตัวดำเนินการที่กล่าวถึงก่อนหน้านี้กับความสามารถในการเป็นค่าว่างก่อนที่จะเข้าถึงพร็อพเพอร์ตี้และฟังก์ชัน สำหรับ สถานการณ์บางอย่างเหล่านี้ 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

วิธีหนึ่งในการจัดการกับสถานการณ์นี้คือการประกาศมุมมองเป็น Nullable และ เริ่มต้นมุมมองโดยเร็วที่สุด ดังที่แสดงในตัวอย่างต่อไปนี้

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

แม้ว่าการดำเนินการนี้จะทำงานตามที่คาดไว้ แต่ตอนนี้คุณต้องจัดการความสามารถในการเป็นค่าว่างของ 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 ดังนั้นโปรด เริ่มต้นพร็อพเพอร์ตี้โดยเร็วที่สุด