28 April 2018

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

Updated on


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

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

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

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

private fun saySomething(context: Context, message: String) {
    val awesomeMessage = message.replace("iOS", "Android")
    Toast.makeText(context, awesomeMessage, Toast.LENGTH_SHORT).show()
}

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

saySomething(getContext(), null)

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


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

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

private fun saySomething(context: Context, messageResId: Int) {
    val awesomeMessage = context.resources.getString(messageResId).replace("iOS", "Android")
    Toast.makeText(context, awesomeMessage, Toast.LENGTH_SHORT).show()
}

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

saySomething(getContext(), R.color.colorAccent)

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

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

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

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

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

public void saySomething(Activity context, @StringRes int resId) {
    String awesomeMessage = activity.getString(resId).replaceAll("iOS", "Android");
    Toast.makeText(activity, awesomeMessage, Toast.LENGTH_SHORT).show();
}

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

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


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

implementation 'com.android.support:support-annotations:27.1.1'

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

        Annotation Support มีให้เลือกใช้งานหลายตัวมากขึ้นอยู่กับความต้องการ แต่เจ้าของบล็อกขอหยิบแค่บางส่วนมาเล่าให้ฟังนะ ถ้าอยากจะดูทั้งหมดก็สามารถเข้าไปดูรายละเอียดแบบเต็มๆกันได้ที่ Annotation Support - 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
public fun doSomething() {
    ...
}

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

Testing Annotation

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

@VisibleForTesting

        @VisibleForTesting เป็น Annotation ที่จะช่วยให้นักพัฒนาสามารถเขียนเทสใน Private, Protected หรือ Package Private ได้ เวลาเขียนในโค้ดก็ให้กำหนดเป็น Public Method ซะ แล้วใส่ Annotation ตัวนี้ เวลาที่ Compile เป็น APK มันจะเปลี่ยนเป็น Private Method ให้เอง

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

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

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

สรุป

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

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