คงไม่มีนักพัฒนาคนไหนที่ไม่รู้จักกับ Intent เนอะ เพราะว่าเป็นหนึ่งในพื้นฐานที่จะได้เรียนและทำความรู้จักกับมันในช่วงแรกๆ แต่ในขณะเดียวกัน PendingIntent ก็เรียกได้ว่ามีแค่นักพัฒนาบางคนเท่านั้นที่รู้จัก เพราะเป็นอะไรที่ไม่ค่อยได้ใช้งานบ่อยเท่า Intent ซึ่งบทความนี้จะพามาทำความรู้จักกับทั้ง 2 คลาสนี้กันครับ

Intent : เห็นกันตั้งแต่สมัยหัดเขียนใหม่ๆ

นักพัฒนาทุกคนรู้จักกับ Intent ด้วยความสามารถที่ทำให้ Activity สั่งเปิด Activity ตัวอื่นๆที่ต้องการได้ เป็นที่มาของการทำแอปฯที่มีหลายๆ Activity หรือหลายๆหน้านั่นเอง

แต่ถ้าว่ากันตามนิยามจริงๆของ Intent แล้วคือ มาตรฐานของรูปแบบการสื่อสารที่ใช้ในแอนดรอยด์ (Standard Messaging Mechanism in Android) ครับ ( อ้างอิงจาก StackOverflow) มีไว้เพื่อให้ Component ในแต่ละส่วนของแอนดรอยด์สามารถสื่อสารข้อมูลกันได้

ซึ่ง Component ที่ว่าก็คือ

  • Activity
  • Broadcast Receiver
  • Service

ถ้าสังเกตดีๆจะเห็นว่า Component ทั้ง 3 ตัว จะต้องมีการประกาศไว้ใน Android Manifest ทุกครั้งเมื่อสร้างขึ้นมาใช้งาน

โดย Intent นั้นถูกแบ่งการทำงานออกแบบ 2 แบบด้วยกันคือ

  • Explicit Intent
  • Implicit Intent

Explicit Intent

เป็นรูปแบบการใช้งาน Intent ที่มีการระบุ Component หรือ Class ปลายทางอย่างเจาะจง

ตัวอย่างคำสั่ง

กรณีของ Activity

สั่งให้ Activity ที่กำหนด เริ่มทำงานทันที

val context: Context = ...
val intent = Intent(context, TimelineActivity::class.java)
startActivity(intent)

val intent = Intent()
intent.setClass(context, TimelineActivity::class.java)
startActivity(intent)

val intent = Intent()
val componentName = ComponentName([email protected], TimelineActivity::class.java)
intent.component = componentName
startActivity(intent)

กรณีของ BroadcastReceiver 

สั่งให้ BroadcastReceiver ที่กำหนด เริ่มทำงานทันที

val context: Context = ...
val intent = Intent(context, UpdateReceiver::class.java)
sendBroadcast(intent)

val intent = Intent()
intent.setClass(context, UpdateReceiver::class.java)
sendBroadcast(intent)

val intent = Intent()
val componentName = ComponentName(context, UpdateReceiver::class.java)
intent.component = componentName
sendBroadcast(intent)

กรณีของ Service 

สั่งให้ Service ที่กำหนด เริ่มทำงานทันที

val context: Context = this
val intent = Intent(context, UploadPhotoService::class.java)
startService(intent)

val intent = Intent()
intent.setClass(context, UploadPhotoService::class.java)
startService(intent)

val intent = Intent()
val componentName = ComponentName(context, UploadPhotoService::class.java)
intent.component = componentName
startService(intent)
โค้ดเหล่านี้ เป็นแค่ตัวอย่างการเรียกใช้งานอย่างง่ายที่สุดเท่านั้น ในการใช้งานจริงอาจจะมีการกำหนดค่าบางอย่างเข้าไปด้วย

ไม่ว่าจะแบบไหนก็เป็นการสร้าง Intent และเรียกใช้งานแบบ Explicit ทั้งหมด เพราะมีการระบุ Component หรือ Class ปลายทาง และนั่นก็หมายความว่าคำสั่งเปิด Activity ตัวใหม่ขึ้นมาที่เรียกใช้งานกันอยู่บ่อยๆ ก็เป็น Explicit Intent นั่นเอง

และถ้าอยากส่งข้อมูลใดๆให้ปลายทางก็จะมีคลาส Bundle ให้แนบข้อมูลจำพวก Primitive Data Type เพื่อส่งไปให้ปลายทางได้

val context: Context = this
val value = 30
val intent = Intent(context, TimelineActivity::class.java)
intent.putExtra("key_value", value)
startActivity(intent)

val value = 30
val bundle = Bundle()
bundle.putInt("key_value", value)
val intent = Intent(context, TimelineActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)

ซึ่ง Intent ทุกตัวสามารถมีข้อมูลแนบมาได้ ดังนั้นปลายทางที่ Intent ส่งไปก็สามารถดึงข้อมูลแบบนี้ได้ทันทีไม่ว่าจะเป็น Activity, BroadcastReceiver หรือ Service

Implicit Intent

เป็นรูปแบบการใช้งาน Intent ที่ไม่ได้ระบุ Component หรือ Class อย่างเจาะจง จะเป็นตัวไหนก็ได้ ขอแค่ตรงกับเงื่อนไขที่กำหนดไว้ก็พอ ซึ่งเงื่อนไขที่ว่าก็คือ Action, Type และ Category

การทำงานของ Implicit Intent จะเป็นการส่ง Intent ไปที่ Android System โดยมีการระบุเงื่อนไขไว้ว่าต้องการปลายทางที่มี Action, Type หรือ Category แบบไหน แล้วเจ้า Android System ก็จะไปดูในเครื่องว่ามีตัวไหนบ้างที่รองรับตามที่กำหนดไว้ (ถ้ามีมากกว่าหนึ่งตัวก็จะให้ผู้ใช้เป็นคนเลือก) แล้วจึงส่ง Intent เพื่อไปสั่งให้เป้าหมายเริ่มทำงาน

Intent แบบไม่เจาะจงปลายทางนั้นเป็นยังไง? ลองนึกภาพเวลาผู้ที่หลงเข้ามาอ่านกดแชร์ภาพซักภาพจากใน Gallery ดูครับ

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

นั่นล่ะครับที่เรียกว่า Implicit Intent

ตัวอย่างคำสั่ง

กรณีของ Activity

val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, "Hello Android Fragmentation World")
intent.type = "text/plain"
startActivity(intent)

val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_TEXT, "Hello Android Fragmentation World")
intent.type = "text/plain"
startActivity(intent)

กรณีของ BroadcastReceiver

val intent = Intent(Intent.ACTION_SCREEN_ON)
sendBroadcast(intent)

val intent = Intent()
intent.action = Intent.ACTION_SCREEN_ON
sendBroadcast(intent)

สำหรับ Service จะไม่นิยมใช้ Explicit Intent เพราะว่าการทำงานของ Service มักจะเป็นการเรียกใช้งานแบบเจาะจงเท่านั้น หรือไม่ก็ส่งไปที่ BroadcastReceiver แล้วเรียกให้ Service ทำงานอีกทอดแทน

ทั้ง 2 แบบแตกต่างกันที่รูปแบบการระบุปลายทาง จึงทำให้ Explicit Intent นิยมใช้เรียก Component ที่อยู่ภายในแอปฯตัวเดียวกันซะมากกว่า ส่วน Implicit Intent ก็จะนิยมใช้เรียกข้ามแอปฯกัน

แต่ถ้าถามว่า สามารถใช้ Explicit Intent ในเพื่อเรียก Component จากแอปฯตัวอื่นๆได้มั้ย? บอกเลยว่าได้ครับ แต่ผู้ที่หลงเข้ามาอ่านจะต้องระบุ Component ปลายทางแบบเจาะจงด้วยนะ (ก็มันคือ Explicit Intent นี่นา)

val packageName = "com.google.android.apps.photos"
val fullClassName = "com.google.android.apps.photos.home.HomeActivity"
val intent = Intent()
intent.component = ComponentName(packageName, fullClassName)
startActivity(intent)

คำสั่งข้างบนนี้คือการเปิดหน้า Home ของ Google Photos

แต่ Google Photos ไม่ได้มีอยู่ในอุปกรณ์แอนดรอยด์ทุกเครื่องนี่นา เพราะบางคนก็ไม่ได้ลง Google Photos ไว้ นั่นหมายความว่าคำสั่งนี้จะไร้ค่าไปในทันที เมื่อเครื่องนั่นๆไม่มี Google Photos

  • Explicit Intent เหมาะกับ Component ในแอปฯเดียวกัน เพราะรู้ปลายทางแน่นอน ในกรณีที่ต้องใช้กับ Component ของแอปฯตัวอื่น ควรมีการเช็คว่าแอปฯปลายทางมีอยู่ในเครื่องจริงๆหรือไม่
  • Implicit Intent เหมาะกับ Component ระหว่างแอปฯด้วยกัน เนื่องจากไม่สามารถเจาะจงปลายทางได้อย่างแน่นอน ถึงแม้ว่าจะสามารถใช้กับ Component ภายในแอปฯเดียวกันได้ แต่จะใช้ทำไมในเมื่อรู้ปลายทางแน่นอนอยู่แล้ว (ใช้ Explicit Intent แทนดีกว่ามั้ย?)

Parameter ต่างๆสำหรับ Implicit Intent

เนื่องจาก Explicit Intent กับ Implicit Intent ต่างกันตรงที่การเรียก Component ปลายทาง ดังนั้นวิธีการกำหนดค่าใน Intent ก็จะต่างกันด้วยเช่นกัน เพราะว่า Explicit Intent จะเจาะจงปลายทางแน่นอนอยู่แล้ว แต่สำหรับ Implicit Intent นี่สิ จะทำยังไง?

Implicit Intent สามารถกำหนด Parameter ต่างๆที่ใช้ระบุปลายทางได้ดังนี้

  • Action : รูปแบบ Action ของ Component ปลายทาง
  • Type : ประเภทข้อมูล ถึงแม้ว่า Android System จะรู้อยู่แล้วว่าข้อมูลที่ส่งผ่าน Implicit Intent เป็นรูปแบบไหน แต่การกำหนด Type ก็จะช่วยกรองรูปแบบข้อมูลของ Component ปลายทางที่ไม่ต้องการออกไปได้
  • Category : หมวดหมู่ของ Component ปลายทาง

สามารถกำหนด Parameter อย่างใดอย่างหนึ่งหรือทั้งหมดก็ได้ ขึ้นอยู่กับรูปแบบที่ Component ปลายทางต้องการ

แต่ Action ถือว่าเป็นเป็น Parameter สำคัญที่ Implicit Intent ทุกตัวต้องมี เพราะ Component ปลายทางจะมีการกำหนดไว้ว่าตัวไหนรองรับ Action แบบไหน เพื่อที่จะได้รองรับการทำงานได้ตรงตามกับ Action ดังนั้นอยากจะใช้ Implicit Intent เพื่อให้ Component ปลายทางทำอะไรก็ควรกำหนด Action ให้ถูกต้อง

ตัวอย่าง Action สำหรับ Activity

  • ACTION_VIEW
  • ACTION_EDIT
  • ACTION_CHOOSER
  • ACTION_CALL
  • ACTION_SEND

ตัวอย่าง Action สำหรับ BroadcastReceiver

  • ACTION_BOOT_COMPLETED
  • ACTION_POWER_CONNECTED
  • ACTION_SCREEN_ON
  • MEDIA_MOUNTED

สำหรับ Action ของ BroadcastReceiver ที่มีให้ใช้งานนั้น มักจะเรียกใช้งานจาก BroadcastReceiver ปลายทางที่สร้างขึ้นเองมากกว่า เพราะ Action มาตรฐานเหล่านี้จะถูกส่งจาก Android System เป็นหลักอยู่แล้ว ถ้าจะมีการใช้ Action สำหรับ Broadcast ก็จะเป็น Custom Action เพื่อใช้งานเองซะมากกว่า

ซึ่งจริงๆแล้ว Action เหล่านี้มันก็คือ String ธรรมดาๆนั่นเอง

val ACTION_GET_CONTENT = "android.intent.action.GET_CONTENT"

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

Action with Data

Action สำหรับ Activity บางตัวอาจจะต้องมีการกำหนด Data ที่อยู่ในรูป Uri ด้วย เพื่อให้สามารถทำงานได้ ยกตัวอย่างเช่น เจ้าของบล็อกอยากให้ในแอปฯมีปุ่มกดเพื่อโทรออกไปยังเบอร์ที่ต้องการ ก็จะใช้คำสั่ง Implicit Intent แบบนี้

val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:1150")
startActivity(intent)


คำสั่งข้างบนเป็นแค่บางส่วนเท่านั้น จริงๆแล้วต้องประกาศ Permission ใน Android Manifest และครอบด้วยคำสั่งสำหรับ Runtime Permission เพื่อให้ทำงานบน Android 6.0 Marshmallow อย่างถูกต้อง

Action with Type

Action สำหรับ Activity บางตัวก็ต้องมีการกำหนด Type ด้วย โดยเฉพาะ Action ที่ต้องการให้ส่งข้อมูลบางอย่างกลับมา เช่น แอปฯของเจ้าของบล็อกสามารถเลือกรูปจาก Gallery มาใช้ในแอปฯได้ ก็เลยทำปุ่มให้กดเปิด Gallery ในเครื่องเพื่อให้ผู้ใช้เลือกรูปที่ต้องการ แล้ว Gallery ก็จะส่งข้อมูลกลับมาให้

private val GALLERY_REQUEST_CODE = 99

fun openGalleryImagePicker() {
    val intent = Intent(Intent.ACTION_GET_CONTENT)
    intent.type = "image/*"
    startActivityForResult(intent, GALLERY_REQUEST_CODE)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == GALLERY_REQUEST_CODE && resultCode == RESULT_OK) {
        // Do something here
    }
}

สำหรับการใช้ Implicit Intent เพื่อให้ผู้ใช้เลือกข้อมูลจากแอปฯตัวอื่นๆ จะต้องใช้คำสั่ง startActivityForResult แทน (เพราะมีการส่งข้อมูลกลับมา) ส่วนผลลัพธ์ที่ได้ก็จะถูกส่งเข้ามาใน onActivityResult

ส่วน Request Code เป็นเลขใดๆก็ได้ ถ้ามีการเรียก Implicit Intent ที่ทำงานคล้ายๆกันหลายๆตัว ควรจะเป็นเลขที่ไม่ซ้ำกัน เช่น ในหน้านั้นๆสามารถเลือกรูปจาก Gallery และเลือกเพลงจากใน File Explorer ได้ ควรจะมี Request Code แยกกัน เพื่อให้สามารถแยก Action ได้ เพราะทั้งคู่มี Action ที่เหมือนกัน ต่างกันที่ Type ของข้อมูล

และกรณีที่ Activity ปลายทางมีการส่งข้อมูลกลับมาให้เมื่อทำงานเสร็จ ก็สามารถส่งข้อมูลกลับด้วยโค้ดแบบนี้ได้เลย

val EXTRA_ID = "extra_id"
val EXTRA_NAME = "extra_name"
...
val id: String = ...
val name: String = ...
val intent = Intent().apply { 
    putExtra(EXTRA_ID, id)
    putExtra(EXTRA_NAME, id)
}
setResult(Activity.RESULT_OK, intent)
finish()

จะเห็นว่าสามารถกำหนด Intent ที่จะส่งกลับไปยัง Activity ก่อนหน้าได้ด้วยคำสั่ง setResult ที่สามารถกำหนด Result Code ได้ตามใจชอบ และถ้ามีข้อมูลที่จะส่งกลับไปก็แนบ Intent เข้าไปได้ด้วย

โดยข้อมูลดังกล่าวก็จะแนบกลับไปใน onActivityResult นั่นเอง

val EXTRA_ID = "extra_id"
val EXTRA_NAME = "extra_name"

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == GALLERY_REQUEST_CODE && resultCode == RESULT_OK) {
        val id = data?.getStringExtra(EXTRA_ID)
        val name = data?.getStringExtra(EXTRA_NAME)
        ....
    }
}

Action with Category

และนอกจากนี้บาง Action ก็เรียกใช้งานคู่กับ Category เช่นกัน (ก็ขึ้นอยู่กับ Component ปลายทางนั่นแหละ)

ยกตัวอย่างเช่น เจ้าของบล็อกต้องการเรียกแสดงรายชื่อแอปฯทั้งหมดที่แสดงอยู่ในหน้า Launcher ของเครื่อง

เจ้าของบล็อกจะต้องใช้คำสั่งแบบนี้

val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_LAUNCHER)
startActivity(intent)

สำหรับคำสั่ง Category จะเป็น addCategory แทน เพื่อให้สามารถเพิ่ม Category ได้มากกว่าหนึ่งรูปแบบ

ตัวอย่าง Category ที่มีใน Android System

  • CATEGORY_DEFAULT
  • CATEGORY_LAUNCHER
  • CATEGORY_PREFERENCE
  • CATEGORY_CAR_DOCK
  • CATEGORY_APP_MUSIC

ซึ่งเบื้องหลังของ Category ก็คือ String แบบเดียวกับ Action นั่นเอง และสามารถ Custom เพื่อใช้งานเองได้ด้วย

val CATEGORY_APP_MUSIC = "android.intent.category.APP_MUSIC"

Action with Extra Data

อย่างที่รู้กันอยู่แล้วว่า Intent สามารถแนบข้อมูลผ่าน Bundle ไปได้ด้วย ดังนั้น Implicit Intent ก็สามารถแนบข้อมูลผ่าน putExtra ได้เลย ขึ้นอยู่กับว่าปลายทางต้องการข้อมูลอะไรบ้าง

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

val intent = Intent(Intent.ACTION_SENDTO)
intent.type = "text/plain"
intent.data = Uri.parse("mailto:[email protected]")
intent.putExtra(Intent.EXTRA_SUBJECT, "Why you still miss him?")
intent.putExtra(Intent.EXTRA_TEXT, "Just open your eyes, better guy might be around you")
startActivity(Intent.createChooser(intent, "Send email via"))

การครอบ Intent อีกชั้นด้วยคำสั่ง Intent.createChooser(Intent intent, String message) จะทำให้ผู้ที่หลงเข้ามาอ่านสามารถกำหนดคำที่จะแสดงตอนเลือกแอปฯได้ตามใจชอบ

ตัวอย่าง Extra Data ที่มีใน Android System

  • EXTRA_EMAIL
  • EXTRA_PHONE_NUMBER
  • EXTRA_STREAM
  • EXTRA_SUBJECT
  • EXTRA_TITLE

อย่าลืมนะครับว่าจะต้องใช้ Action แบบไหน, Type อะไร, แนบ Data ด้วยหรือป่าว, ต้องกำหนด Category มั้ย และต้องส่ง Extra Data อะไรบ้างนั้นขึ้นอยู่กับแอปฯปลายทางล้วนๆ ว่ากำหนดรูปแบบการรับข้อมูลไว้ยังไง ผู้ที่หลงเข้ามาอ่านต้องไปหาข้อมูลเอาเอง หรือจะไปดูรูปแบบการส่งข้อมูลผ่าน Intent เบื้องต้นที่ทางทีมพัฒนาแอนดรอยด์ได้กำหนดไว้ก็ได้ Common Intents [Android Developers]

Flag

เป็นค่าที่สามารถกำหนดรูปแบบการทำงาน Task และ Backstack ของปลายทางได้

val context: Context = ...
val intent = Intent(context, TimelineActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)

คำสั่งข้างบนนี้จะเป็นการสร้าง TimelineActivity ขึ้นมาพร้อมกับเคลียร์ Activity Task ที่อยู่ใน Backstack ทิ้งทั้งหมด

ตัวอย่าง Flag ที่มีใน Android System ซึ่งจะแยกตาม Component

  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_SINGLE_TOP
  • FLAG_RECEIVER_FOREGROUND
  • FLAG_RECEIVER_NO_ABORT

Intent Filter — ส่งมาแบบไหนไม่รู้ แต่จะเอาแค่แบบนี้

รู้ตัวอีกทีก็เผลออธิบายเรื่อง Intent ซะยาว มาถึงตอนนี้ผู้ที่หลงเข้ามาอ่านบางคนก็อาจจะสงสัยต่ออีกว่า

แล้วฝั่ง Component ปลายทางกำหนดรูปแบบของ Implicit Intent ไว้ยังไงล่ะ?

จะไม่อธิบายก็ไม่ได้ เดี๋ยวไม่ครบหลักสูตร ดังนั้นจึงขอหยิบเรื่อง Intent Filter ที่เป็นอีกหนึ่งหัวใจสำคัญในการทำงานของ Intent มาอธิบายให้ครบ

เพื่อให้ง่ายต่อการทำงานร่วมกับ Implicit Intent ทางแอนดรอยด์จึงได้มีสิ่งที่เรียกว่า Intent Filter ขึ้นมาให้ใช้ นั่นก็คือ <intent-filter /> ที่ใช้ใน Android Manifest จ้า คุ้นๆมั้ยเอ่ย?

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.akexorcist.myscratch2">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
             <intent-filter> 
                 <action android:name="android.intent.action.MAIN" /> 

                 <category android:name="android.intent.category.LAUNCHER" /> 
             </intent-filter> 
        </activity>
    </application>

</manifest>

เพราะมันมีให้เห็นอยู่ใน Android Manifest มาตั้งแต่แรกที่สร้างโปรเจคเลยนะ

โดยเจ้า Intent Filter จะช่วยให้ผู้ที่หลงเข้ามาอ่านสามารถกำหนดได้ว่า Component (Activity, BroadcastReceiver และ Service) ตัวไหนรับ Parameter ของ Implicit Intent แบบไหนได้บ้าง

ยกตัวอย่างจากอันที่มีอยู่เลยละกัน

<activity android:name=".Main2Activity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
  • Action : android.intent.action.Main หรือ Intent.ACTION_MAIN เป็นการกำหนดว่าให้ Activity ตัวนี้เป็น Activity หลักของแอปฯตัวนี้ (เมื่อแอปเริ่มทำงานจะเข้า Activity ตัวนี้เป็นตัวแรก)
  • Category : android.intent.category.LAUNCHER หรือ Intent.CATEGORY_LAUNCHER เป็นการกำหนดว่าให้สร้าง App Icon ไว้ที่หน้า Launcher ของแอนดรอยด์ และเมื่อกดที่ App Icon ดังกล่าวก็จะเป็นการสั่งเปิด Activity ตัวนี้ทันที

การที่แอปติดตั้งลงในเครื่องแล้วมี App Icon โผล่ขึ้นมา เป็นเพราะการกำหนดค่าแบบนี้ลงใน Android Manifest นั่นเอง

Intent Filter ก็คือ Syntax ที่อยู่ในรูป XML ที่จะต้องใส่ไว้ใน Android Manifest เพื่อให้ Android System สามารถตรวจสอบได้ง่ายว่า Component ตัวไหนรองรับ Explicit Intent แบบไหนบ้าง

โดยจะกำหนดไว้ใน Activity, BroadcastReceiver หรือ Service ตัวไหนก็ได้ตามที่ต้องการ (ให้มันเหมาะสมกับการทำงานของ Component นั้นๆด้วย)

ยกตัวอย่างเช่น เจ้าของบล็อกสร้างแอปฯ Social ของตัวเองขึ้นมา แล้วอยากจะให้ผู้ใช้กดแชร์ข้อความจากที่ไหนก็ได้แล้วให้ขึ้นแอปฯตัวนี้อยู่ในรายชื่อด้วย

เตรียม Intent Filter ใน Android Manifest ให้พร้อม

ก่อนอื่นจะต้องรู้ว่าคำสั่งต้นทางนั้นจะมีหน้าตาเป็นยังไง น่าจะประมาณนี้ละมั้ง

val message = "Welcome to Sleeping For Less"
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, message)
startActivity(intent)

เมื่อรู้คำสั่งต้นทางแล้ว ก็ประกาศ Intent Filter ให้ตรงกัน ดังนี้

<activity android:name=".PostStatusActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
</activity>

และถ้ามี Mime Type หลายแบบใน Component เดียวกัน ก็ประกาศแบบนี้ได้เลย

<activity android:name=".PostStatusActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
        <data android:mimeType="image/*"/>
    </intent-filter>
</activity>

หรือถ้าต้องการแบ่ง Intent Filter เป็นสองชุดใน Component เดียวกันก็สามารถทำแบบนี้ได้เลย

<activity android:name=".PostStatusActivity">
    <intent-filter>
        <action android:name="android.intent.action.EDIT"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
        <data android:mimeType="image/*"/>
        <data android:mimeType="video/*"/>
    </intent-filter>
</activity>

อ่านค่าจาก Implicit Intent ใน Activity/BroadcastReceiver/Service

ถ้า Intent Filter ตรงกับ Component ที่เตรียมไว้ ก็จะทำให้ Component นั้นๆทำงานทันที โดยค่าต่างๆที่ถูกส่งมาจากต้นทางสามารถเช็คได้จากค่า Intent ที่อยู่ในคลาสนั้นๆ

การรับค่า Implicit Intent ใน Activity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val action: String? = intent.action
        val category: Set<String> = intent.categories
        val uri: Uri? = intent.data
        val mimeType: String? = intent.type
        val message: String? = intent.getStringExtra(Intent.EXTRA_TITLE)
        ...
    }
}

การรับค่า Implicit Intent ใน BroadcastReceiver

class StartUpBroaderReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val action: String? = intent.action
        val category: Set<String> = intent.categories
        val uri: Uri? = intent.data
        val mimeType: String? = intent.type
        val message: String? = intent.getStringExtra(Intent.EXTRA_TITLE)
        ...
    }
}

การรับค่า Implicit Intent ใน Service

class UploadService : Service() {
    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        val action = intent.action
        val category = intent.categories
        val uri = intent.data
        val mimeType = intent.type
        val message = intent.getStringExtra(Intent.EXTRA_TITLE)
        ...
        return super.onStartCommand(intent, flags, startId)
    }
    ...
}

ที่เหลือก็จัดการเองตามใจชอบเลยจ้า

Pending Intent : ไม่ค่อยเป็นที่จดจำ แต่ก็สำคัญเหมือนกันนะ

PendingIntent เป็น Wrapper ที่มี Intent อยู่ข้างใน เอาไว้ใช้งานบางอย่างที่ยังไม่อยากให้ Intent ณ ตอนนั้น แต่อยากจะให้รอจนกว่าจะถึงเงื่อนไขที่กำหนด แล้ว Intent ที่อยู่ในนั้นค่อยทำงาน

เมื่อเทียบกันระหว่าง Intent กับ PendingIntent ก็จะประมาณนี้ล่ะนะ

  • Intent ทำงานทันทีเมื่อสั่งงาน
  • PendingIntent เก็บ Intent ไว้ เมื่อสั่งงานก็จะยังไม่ทำงานในทันที ต้องรอจนกว่าจะเป็นไปตามเงื่อนไขที่กำหนดไว้

มีการทำงานแบบไหนที่ต้องส่ง Intent ให้ไปเก็บไว้ก่อนด้วยหรอ? ลองนึกถึงการทำงานของ NotificationManager กับ AlarmManager ดูสิ

เวลาที่ Notification ทำงาน (แสดงอยู่บน Notification Bar) ก็ใช่ว่าผู้ใช้จะกดทันทีซะหน่อย ดังนั้นมันอาจจะถูกปล่อยทิ้งไว้พักใหญ่ๆแล้วค่อยถูกกดก็ได้ นั่นล่ะ!! PendingIntent จะถูกใช้ใน NotificationManager เพื่อเก็บ Intent ไว้จนกว่าผู้ใช้จะกดที่แถบ Notification นั่นเอง

อาจจะสงสัยกันต่อว่าทำไมต้องลำบากสร้าง PendingIntent ด้วย ทำไมไม่โยน Intent ไปให้มันเก็บตั้งแต่แรกล่ะ? นั่นก็เพราะว่า Intent ไม่สามารถ Execute ได้ด้วยตัวเอง แต่ PendingIntent สามารถทำได้

ดังนั้นโดยหลักการทำงานของ NotificationManager แล้ว จะต้องสร้าง Intent กับ PendingIntent แล้วเอาไปเก็บไว้ใน NotificationManager

ตัวอย่างคำสั่ง Notification ที่มี PendingIntent ด้วย โดยให้กดที่แถบ Notification แล้วเปิดหน้าเว็ป https://akexorcist.dev ผ่าน ACTION_VIEW

val context: Context = ...
val channelId: String = ...
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://akexorcist.dev"))
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val builder = NotificationCompat.Builder(context, channelId).apply {
    setContentText("Content Text")
    setContentTitle("Content Title")
    setSmallIcon(R.drawable.ic_notification)
    setContentIntent(pendingIntent)
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(0, builder.build())

ซึ่งผู้ที่หลงเข้ามาอ่านสามารถสร้าง Intent แบบไหนก็ได้ ตามต้องการ จะสั่งเปิด Activity หรือ Service ก็ได้หมด (เท่าที่ Intent มันทำได้น่ะแหละ)

ทีนี้มาดูที่คำสั่งตอนสร้าง PendingIntent นิดหน่อยดีกว่า

val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)

เผื่อไม่ให้งงว่าในตัวอย่างกำหนดค่าอะไรลงไปบ้าง

PendingIntent.getActivity(context: Context, requestCode: Int, intent: Intent, flags: Int)

จะเห็นว่าเป็น Parameter ของ Intent ทั้งหมดเลย ซึ่งเดี๋ยว PendingIntent เอาไปกำหนดลงใน Intent ให้เองครับ ส่วน Request Code จะใช้กรณีที่รูปแบบการทำงานมีหลายแบบ (เช่น Notification มีหลายประเภท) การกำหนด Request Code จะช่วยให้ Component ปลายทางสามารถแยกการทำงานได้ครับ

สรุป

Intent ถือว่าเป็นพื้นฐานสำคัญที่อยากให้ลองทำความเข้าใจถึงการทำงานและความสามารถของมันครับ ทั้งนี้ต้องดูเรื่องของ Intent Filter ควบคู่กันด้วย เพราะจะช่วยให้เข้าใจรูปแบบการทำงานที่ทีมพัฒนาแอนดรอยด์วางไว้ให้ครับ

ส่วน PendingIntent อาจจะไม่ได้ใช้งานบ่อยมากนัก เพราะจะเน้นไปที่เวลาเรียกใช้งาน NotificationManager กับ AlarmManager ซะมากกว่า แต่ก็อาจจะมี 3rd Party Application บางตัวที่ใช้ PendingIntent ในการทำงานด้วยนะ (แต่ไม่รู้ตัวไหนมีบ้าง)

ก็หวังว่าบทความนี้จะช่วยให้ผู้ที่หลงเข้าใจเกี่ยวกับ Intent, Intent Filter และ PendingIntent มากขึ้นนะฮะ (อุตส่าห์เขียนซะยาวเหยียดขนาดนี้)

แหล่งข้อมูลอ้างอิง