มาทำชีวิตให้ง่ายขึ้น เขียนโค้ดให้ดีขึ้นด้วย 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 ด้วย ทำให้โอกาสที่โค้ดของเราจะทำงานผิดพลาดจากเรื่องไม่เป็นไรให้ลดน้อยลง