มาทำชีวิตให้ง่ายขึ้น เขียนโค้ดให้ดีขึ้นด้วย AndroidX Annotation กันเถอะ

Android Jetpack ถือว่าเป็น Library ที่ช่วยให้นักพัฒนาแอนดรอยด์มีชีวิตที่สะดวกสบายขึ้นเยอะมาก และในวันนี้ขอแนะนำให้รู้จักกับหน่ึงใน Android Jetpack ที่ชื่อว่า AndroidX Annotation กันนนนนนนนนน

รู้จัก Lint กันแล้วหรือยัง?

นักพัฒนาแอนดรอยด์แทบทุกคนนั้นคุ้นเคยกับสิ่งที่เรียกว่า Lint ที่อยู่ใน Android Studio กันเป็นอย่างดี เพราะมันจะคอยช่วยเช็คให้ว่าโค้ดที่เขียนลงไปนั้นผิด Syntax หรือไม่ และที่ฉลาดไปกว่านั้นมันสามารถช่วยเช็คได้ด้วยว่าโค้ดตรงไหนอาจจะเกิดปัญหาในตอน Runtime ได้

สมมติว่าเจ้าของบล็อกเขียนโค้ดแบบนี้ขึ้นมา

fun doSomething(context: Context, message: String) {
    /* ... */
}

แล้ววันหนึ่งเผลอไปเรียก Method แล้วใส่ค่า null หรือค่าที่อาจจะทำให้เกิดเป็น null ได้แบบนี้

val context: Context = /* ... */
doSomething(
    context = context,
    message = null
)

เจ้า Lint อันน่ารักนี้ก็จะคอยเตือนให้เจ้าของบล็อกรู้ว่า เอ้ย ถ้าทำแบบนี้แล้วแอปฯจะบึ้มได้นะ! เพราะงั้นห้าม Compile เด็ดขาด!!

แต่ทว่า Lint ก็ไม่สามารถตรัสรู้ได้ซะทุกอย่าง เพราะว่าบ่อยครั้งนักพัฒนาชอบเขียนอะไรก็ไม่รู้ที่มีแต่คนเขียนกับพระเจ้าเท่านั้นที่จะรู้และเข้าใจมันได้

ยกตัวอย่างเช่น เจ้าของบล็อกแก้ไข Method ตัวเดิมให้ดึงค่าจาก String Resource แทนจะได้เรียกใช้งานสะดวกกว่า String โดยตรง

fun doSomething(context: Context, messageResId: Int) {
    /* ... */
}

แต่เนื่องจาก String Resource มันมองเป็น Integer นั่นหมายความว่าถ้าวันไหนเจ้าของบล็อกเมากลับห้องแล้วมานั่งเขียนโค้ดต่อ ก็อาจจะเผลอลั่นคีย์บอร์ดลงไปแบบนี้

val context: Context = /* ... */
doSomething(
    context = context, 
    messageResId = R.color.colorAccent
)

ผลก็คือแอปพังทันทีเพราะ ResourceNotFoundException เนื่องจากเจ้าของบล็อกดันไปเผลอใส่ Color Resource เข้าไปแทน String Resource โดยที่ Lint ไม่สามารถช่วยเตือนปัญหาแบบนี้ได้เลย

อาจจะฟังดูตลก แต่ว่าปัญหาแบบนี้เกิดขึ้นได้บ่อยมาก กลายเป็นว่านักพัฒนาก็ต้องมานั่งเขียนโค้ดเพื่อ Validate ค่าที่ส่งเข้ามาใน Method นี้อีกหรือไม่ก็ใส่ Try-catch ซะเลย โคตรเสียเวลาอ่ะ

และนี่ก็คือที่มาของ AndroidX Annotation นั่นเอง

จากโค้ดตัวอย่างก่อนหน้านี้จะเห็นว่าปัญหาที่เกิดขึ้นเป็นปัญหาเฉพาะโค้ดของแอนดรอยด์เท่านั้น ดังนั้นทีมพัฒนาของ Android จึงสร้าง AndroidX Annotation ขึ้นมาเพื่อให้นักพัฒนาสามารถใส่ Annotation ไว้ในโค้ดเพื่อบอก Lint ให้ทำงานตามเงื่อนไขของ Annotation นั้นๆได้

จากปัญหา String Resource ถ้าไม่อยากให้โยน Resource แบบอื่นๆเข้ามาได้ ก็ให้กำหนด @StringRes ไว้ข้างหน้าตัวแปรที่ต้องการซะ

fun doSomething(context: Context, @StringRes messageResId: Int) {
    /* ... */
}

โดย Annotation ตัวนี้จะไปบอก Lint ว่า “เฮ้ย ตัวแปรตัวนี้เนี่ย รับค่าเป็น String Resource เท่านั้นนะ จะโยนอย่างอื่นเข้ามาไม่ได้นะ!!!”

ถ้าวันไหนเจ้าของบล็อกเมากลับมาแล้วเผลอใส่ Color Resource เข้าไปใน Method ตัวนี้อีกครั้ง Lint มันก็จะรู้แล้วแจ้งให้เจ้าของบล็อกเห็นทันที

นั่นล่ะครับ หน้าที่ของ AndroidX Annotation

โดย Library ตัวนี้จะมาพร้อมกับ AndroidX AppCompat อยู่แล้ว ดังนั้นจึงสามารถใช้งานได้เลย แต่ถ้าในโปรเจคของผู้ที่หลงเข้ามาอ่านไม่ได้ใช้ AppCompat (ไม่ได้ใช้จริง ๆ หรอ…) แล้วอยากจะเรียกใช้งานเฉพาะ AndroidX Annotation อย่างเดียวก็ไปเพิ่มเองใน build.gradle ตามนี้ได้เลย

implementation 'androidx.annotation:annotation:<latest_version>'

แล้ว Annotation มีอะไรให้ใช้บ้าง?

AndroidX Annotation มีให้เลือกใช้งานหลายตัวมากขึ้นอยู่กับความต้องการ แต่เจ้าของบล็อกขอหยิบแค่บางส่วนมาเล่าให้ฟังนะ ถ้าอยากจะดูทั้งหมดก็สามารถเข้าไปดูรายละเอียดแบบเต็มๆกันได้ที่ AndroidX Annotation — Package Summary [Android Developers]

Null Annotation

ใช้สำหรับกำหนดว่าสามารถเป็น null ได้หรือไม่ ซึ่งมีประโยชน์มากในการบอกว่า Method นั้นๆห้ามส่งค่า null มานะ โดยไม่ต้องไปเสียเวลานั่งเขียนโค้ดเพื่อ Validate เพิ่มเข้าไปด้วยการใส่ @NonNull ไว้ข้างบน Method นั้นๆ

หรือจะใส่ @Nullable เพื่อบอกให้รู้ว่า “โยน null เข้ามาได้เลย ข้างในเขียนเผื่อไว้แล้ว” ก็ได้เช่นกันนะ

@NonNull
@Nullable

แต่ Annotation ตัวนี้จะหมดประโยชน์ทันทีเมื่อเขียนเป็น Kotlin ทั้งโปรเจค…

Resource Annotation

จะเป็น Annotation สำหรับเหล่า Resource ทั้งหลายที่แอนดรอยด์รองรับ

@AnimatorRes
@AnimRes
@AnyRes
@ArrayRes
@AttrRes
@BoolRes
@ColorRes
@DimenRes
@DrawableRes
@FontRes
@FractionRes
@IdRes
@IntegerRes
@InterpolatorRes
@LayoutRes
@MenuRes
@NavigationRes
@PluralsRes
@RawRes
@StringRes
@StyleableRes
@StyleRes
@TransitionRes
@XmlRes

ตัวอย่างการใช้งาน

private fun showSomething(context: Context, @StringRes messageResId: Int, @ColorRes messageColorResId: Int) {
    /* ... */
}

จะใช้กับ Return Type ก็ได้นะ สมมติว่า Method นั้นๆส่งค่าออกมาเป็น Resource ใดๆก็ตาม

@StringRes
private fun showSomething(type: Int): Int = when (type) {
    TYPE_POST -> R.string.post_message
    TYPE_PHOTO -> R.string.photo_message
    TYPE_VIDEO -> R.string.video_message
    else -> R.string.unknown_message
}

Permission Annotation

เอาไว้บอกว่า Method นั้นๆต้องขอ Permission ก่อนนะถึงจะใช้งานได้

@RequiresPermission
@RequiresPermission.Read
@RequiresPermission.Write

ตัวอย่างการใช้งาน

@RequiresPermission(Manifest.permission.SEND_SMS)
private fun sendMessageViaSms(message: CharSequence) {
    /* ... */
}

@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
private fun getLastKnownLocation(provider: String) {
    /* ... */
}

สำหรับ @RequiresPermission.Read และ @RequiresPermission.Write มีไว้สำหรับ Permission ที่ต้องมีการ Read/Write ข้อมูลในเครื่อง

Thread Annotation

Annotaion สำหรับกำหนด Thread ที่จะเรียกใช้งาน Method นั้นๆได้ จะได้ไม่เผลอเรียกใช้งานผิด Thread

@AnyThread
@BinderThread
@MainThread
@UiThread
@WorkerThread

ตัวอย่างการใช้งาน

@WorkerThread
private fun doEverythingHere() {
    /* ... */
}

Range Annotation

สำหรับบอกให้รู้ว่าค่าที่จะ Return ออกมาจาก Method จะมีค่าอยู่ในช่วงไหน จะได้ไม่ต้องมานั่งเช็คค่าดังกล่าวทีหลัง

@FloatRange
@IntRange

ตัวอย่างการใช้งาน

@IntRange(from = 0, to = 360)
private fun calculateAngle(): Int {
    return /* ... */
}

Define Annotation

เอาไว้กำหนด Constant ที่สร้างขึ้นมาเอง เพื่อกำหนดไว้ใน Method ได้ว่าถ้าจะโยนค่าเข้ามาใน Method นั้นๆจะต้องเป็น Constant ที่กำหนดไว้เท่านั้นนะ (เหมือน getSystemService(...) นั่นเอง)

@IntDef
@LongDef
@StringDef

ตัวอย่างการใช้งาน

@Retention(AnnotationRetention.SOURCE)
@StringDef(PHONE, TABLET, WATCH, TV, AUTO)
annotation class DeviceType {}

companion object {
    const val PHONE = "phone"
    const val TABLET = "tablet"
    const val WATCH = "watch"
    const val TV = "tv"
    const val AUTO = "auto"
}

private fun getConfiguration(@DeviceType type: Int): Configuration {
    /* ... */
}

Color Annotation

สำหรับกำหนดว่าค่าที่ส่งเข้ามาใน Method จะต้องมีค่าที่อยู่ในช่วง Color Integer/Long เท่านั้นนะ

@ColorInt
@ColorLong

@ColorInt จะเป็นช่วงสี ARGB แบบที่ผู้ที่หลงเข้ามาอ่านคุ้นเคยกัน โดยค่าจะอยู่ระหว่าง 0 ถึง 4,294,967,295 ในฐาน 10 หรือ 0x0 ถึง 0xFFFFFFFF ในฐาน 16 นั่นเอง ส่วน @ColorLong จะเป็นค่าสีที่เพิ่มเรื่อง Color Space เข้ามาใหม่ใน Android O

ตัวอย่างการใช้งาน

private fun setTextColor(@ColorInt color: Int) {
    /* ... */
}

Dimension Annotation

เป็น Annotation สำหรับกำหนดให้รู้ว่าค่าที่ส่งเข้ามาในตัวแปรนั้นๆจะต้องเป็น Dimension Unit แบบไหน

@Dimension
@Px

โดย @Dimension จะใช้สำหรับกำหนดว่าค่าที่ส่งเข้ามาจะต้องเป็น Dimension Resource ที่กำหนดหน่วยเป็น DP, SP หรือ PX (กำหนดได้ตามต้องการ)

และ @Px เป็นการบอกว่าค่า Integer ที่ส่งเข้ามาไหนนี้จะถือว่าเป็นหน่วย Pixel ซึ่งจะต่างกับ @Dimension ตรงที่ไม่ได้เป็นค่าจาก Dimension Resource

private fun setTextSize(@Dimension(unit = Dimension.SP) size: Int) {
    /* ... */
} 

private fun setCanvasSize(@Px width: Int, @Px height: Int) { 
    /* ... */
}

API Version Annotation

เป็น Annotation ที่เอาไว้กำหนดใน Method ที่มีคำสั่งที่ต้องบอกให้รู้ว่าคำสั่งในนั้นรองรับกับ API Level ขั้นต่ำสุดเป็นเวอร์ชันอะไร

@RequiresApi

โดย @RequiresApi จะใช้ระบุว่าคำสั่งใน Method นั้นๆจะต้องมีเป็น API Level อะไรขึ้นไปถึงจะเรียกใช้งานได้

@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
fun getSubscriptionInfoList(context: Context): List<SubscriptionInfo> {
    val subscriptionManager = context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager
    return subscriptionManager.activeSubscriptionInfoList
}

เมื่อใดก็ตามที่เผลอเรียกคำสั่งนี้ โดยที่โปรเจคกำหนดไว้ให้รองรับ API Level ที่ต่ำกว่าคำสั่งนี้รองรับได้ Lint ก็จะทำการแจ้งเตือนให้รู้ว่าคำสั่งนี้จะทำงานไม่ได้ถ้า API Level ต่ำกว่าที่ Method นี้รองรับนะ ต้องไปปรับ minSdkVersion ให้สูงกว่านี้หรือว่าใส่โค้ดเพื่อเช็คว่า API Level ของเครื่องนั้นๆรองรับหรือป่าว แล้วค่อยเรียก Method นี้

ProGuard Annotation

เป็น Annotation สำหรับตอนที่ ProGuard กำลังทำงาน

@Keep

โดย @Keep จะเป็นการบอก ProGuard ให้รู้ว่า Class/Method ตัวนี้ไม่ต้องไปลบทิ้งตอนทำ Minify นะ เพราะว่าบาง Class/Method ที่ไม่ได้ถูกเรียกใช้งานโดยตรง จะทำให้ ProGuard เข้าใจผิดได้ง่ายและลบทิ้งออกไป (โดยเฉพาะพวกคำสั่งที่ถูกเรียกใช้งานผ่าน Reflection)

@Keep
fun doSomething() {
    /* ... */
}

ดังนั้นการใช้ @Keep จะช่วยให้นักพัฒนาสามารถระบุเป็นบาง Class หรือบาง Method ได้โดยไม่ต้องไปนั่งใส่ ProGuard Rules ให้เสียเวลา

Testing Annotation

Annotation สำหรับชาวเขียนเทส

@VisibleForTesting

@VisibleForTesting เป็น Annotation ที่ใช้สำหรับ Method ที่จำเป็นต้องทำเป็น Public Method เพื่อให้โค้ดสำหรับเขียนเทสสามารถเรียกใช้งานได้โดยตรง

@VisibleForTesting
fun somePrivateMethod(context: Context): String {
    /* ... */
}

@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
fun someProtectedMethod(context: Context): String {
    /* ... */
}

@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
fun somePackagePrivateMethod(context: Context): String {
    /* ... */
}

ถ้ามีโค้ดนอกเหนือจากการเขียนเทสเรียกใช้งาน Method นี้ในรูปแบบของ Public Method ก็จะโดน Lint แจ้งเตือนให้ทราบว่าเป็น Method ที่กำหนด Package Modifier เป็น Public เพื่อใช้สำหรับเทสเท่านั้น

สรุป

AndroidX Annotation นั้นเป็น Library ที่เตรียม Annotation ไว้ให้นักพัฒนาแอนดรอยด์ทำงานได้สะดวกขึ้น ลดการเขียนโค้ดบางส่วนลงได้โดยให้เป็นหน้าที่ของ Lint แทน และลดความผิดพลาดจากการเอาโค้ดเหล่านั้นไปใช้งานไม่ถูกต้อง

สำหรับผู้ที่หลงเข้ามาอ่านคนใดที่มีโปรเจคอยู่แล้ว และรู้สึกว่าอยากจะเอา AndroidX Annotation ไปใส่เพื่อให้เขียนโค้ดภายหลังได้สะดวกขึ้นก็จัดไปได้เลย เพราะ Annotation เหล่านี้จะช่วยเราในระหว่างเขียนโค้ด และบางตัวก็มีผลในตอน Compile Time ด้วย ทำให้โอกาสที่โค้ดของเราจะทำงานผิดพลาดจากเรื่องไม่เป็นไรให้ลดน้อยลง