ในการส่งข้อมูลระหว่าง Activity/Fragment ↔ Fragment ด้วยกันนั้นเป็นเรื่องที่พบเจอได้บ่อยในระหว่างการพัฒนาแอป โดยนักพัฒนาส่วนใหญ่จะนิยมสร้าง Shared ViewModel เข้ามาช่วยเป็นตัวกลางเพื่อส่งข้อมูลจากอีกที่ไปยังอีกที่
แต่บางครั้งการสร้าง ViewModel เพื่อส่งข้อมูลง่าย ๆ ก็อาจจะดูสิ้นเปลืองไปหน่อย จึงทำให้ทีมแอนดรอยด์ได้พัฒนา Fragment Result API เพื่อเป็นทางเลือกให้กับนักพัฒนาสำหรับการส่งข้อมูลแบบ One-Time Value ระหว่าง Activity/Fragment ←→ Fragment
โดย Fragment Result API จะมีให้ใช้งานใน AndroidX Fragment เวอร์ชัน 1.3.0 ขึ้นไป
implementation 'androidx.fragment:fragment-ktx:<latest_version>'
การทำงานของ Fragment Result API
ในการส่งข้อมูลของ Fragment Result API จะเป็นรูปแบบ Publish-Subscribe Pattern ที่มี FragmentManager เป็นตัวกลาง
และอย่างที่นักพัฒนารู้กันว่า Fragment Manager นั้นเป็น Parent-Child Hierarchy จึงขึ้นอยู่กับว่า Publisher และ Subscriber เป็นใครเมื่อต้องใช้งานกับ Nested Fragment ด้วย
เพราะ Fragment ที่มี Child Fragment อยู่ด้วย เวลาจะส่งข้อมูลด้วย Fragment Result API ก็ต้องเลือก FragmentManager ให้ถูกด้วย
จากภาพข้างบน สมมติว่าต้องการให้ Fragment ในกรอบสีเขียวส่งข้อมูลไปให้ Activity และ Fragment ตัวอื่น ๆ
- Activity - ส่งผ่าน Parent's FragmentManager
- Fragment ใน FragmentManager เดียวกัน - ส่งผ่าน Parent's FragmentManager
- Child Fragment - ส่งผ่าน Child's FragmentManager
นั่นหมายความว่าการส่งข้อมูลระหว่าง Fragment ที่อยู่คนละ FragmentManager จึงไม่สามารถทำได้โดยตรง ต้องทำอย่างน้อย 2-hop ขึ้นไป (ซึ่งไม่แนะนำซักเท่าไร)
การใช้งาน Fragment Result API
เจ้าของบล็อกขอแบ่งวิธีการใช้งาน Fragment Result API เป็น 2 ส่วน คือฝั่ง Subscriber ที่รอรับข้อมูลจากต้นทาง และฝั่ง Publisher ที่จะส่งข้อมูลไปให้ปลายทาง
Subscriber
จะต้องใช้คำสั่ง setFragmentResultListener(...)
เพื่อรอรับค่าจาก Publisher ซึ่งคำสั่งดังกล่าวจะมีอยู่ใน FragmentManager ซึ่งเรียกใช้งานได้ทั้งใน Activity และ Fragment
fun setFragmentResultListener(
requestKey: String,
lifecycleOwner: LifecycleOwner,
listener: FragmentResultListener
)
จะเห็นว่าคำสั่งดังกล่าวจะต้องกำหนดค่าสำหรับ LifecycleOwner ด้วย แปลว่า Fragment Result API จะจัดการเรื่อง Lifecycle ให้โดยอัตโนมัติ (เหมือนกับที่ LiveData ทำ) และข้อมูลที่ Publisher ส่งมาให้จะอยู่ในรูปของ FragmentResultListener
และมาดูที่ requestKey
กันต่อ เพราะการที่ Publisher จะส่งข้อมูลเข้ามาที่ Subscriber ได้ จะต้องกำหนด Request Key ให้เหมือนกันด้วย ซึ่งนักพัฒสามารถกำหนดได้ตามใจชอบ และจะสร้าง Subscriber สำหรับ Request Key หลาย ๆ แบบก็ได้เช่นกัน
val fragmentManager: FragmentManager = /* ... */
val lifecycleOwner: LifecycleOwner = /* ... */
fragmentManager.setFragmentResultListener(
requestKey = "event1",
lifecycleOwner = lifecycleOwner
) { requestKey: String, result: Bundle ->
// Do something
}
fragmentManager.setFragmentResultListener(
requestKey = "event2",
lifecycleOwner = lifecycleOwner
) { requestKey: String, result: Bundle ->
// Do something
}
และเมื่อลองดูที่ FragmentResultListener ก็จะเห็นว่าข้อมูลที่ส่งมาให้จะมีทั้ง Request Key กับข้อมูลที่อยู่ในรูปของ Bundle
val fragmentManager: FragmentManager = /* ... */
fragmentManager.setFragmentResultListener(/* ... */) { requestKey: String, result: Bundle ->
val name = result.getString("name")
val timestamp = result.getLong("timestamp")
/* ... */
}
นั่นหมายความว่าการส่งข้อมูลของ Fragment Result API คือการแนบข้อมูลไว้ใน Bundle นั่นเอง
สำหรับการใช้คำสั่ง setFragmentResultListener
จะขึ้นอยู่กับว่าใช้กับ Activity หรือ Fragment เพราะทั้งคู่มีรูปแบบและเงื่อนไขที่แตกต่างกันเล็กน้อย
ในกรณีที่เป็น Activity ก็จะใช้คำสั่งผ่าน supportFragmentManager
แบบนี้
// MainActivity.kt
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
supportFragmentManager.setFragmentResultListener(
requestKey = "event",
lifecycleOwner = this
) { requestKey: String, result: Bundle ->
// Do something
}
}
}
ส่วน Fragment จะขึ้นอยู่กับว่าจะใช้กับ FragmentManager ที่เป็น Parent หรือ Child
ถ้าต้องการใช้กับ Parent สามารถใช้ Extension Function ที่ Fragment Result API เตรียมไว้ให้แบบนี้ได้เลย
// MainFragment.kt
class MainFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
/* ... */
setFragmentResultListener(
requestKey = "event"
) { requestKey: String, result: Bundle ->
// Do something
}
}
}
ข้อดีของ Extension Function ตัวนี้คือ นักพัฒนากำหนดแค่ Request Key และ FragmentResultListener เท่านั้น ส่วน LifecycleOwner จะถูกกำหนดให้เองโดยอัตโนมัติ
แต่ถ้าต้องการใช้กับ Child ก็จะต้องกำหนดเองทั้งหมดแบบนี้
// MainFragment.kt
class MainFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
/* ... */
childFragmentManager.setFragmentResultListener(
requestKey = "event",
lifecycleOwner = this
) { requestKey: String, result: Bundle ->
// Do something
}
}
}
Publisher
สำหรับการส่งข้อมูลให้ Subcriber จะใช้คำสั่ง setFragmentResult(...)
ที่อยู่ใน FragmentManager ที่จะต้องกำหนด Request Key และข้อมูลที่ต้องการส่งในรูปของ Bundle เสมอ
ถ้าส่งข้อมูลจาก Activity ก็จะต้องเรียกผ่าน supportFragmentManager
นั่นเอง
// MainActivity.kt
class MainActivity: AppCompatActivity() {
private fun sendEventToFragmentManager() {
val result = bundleOf( /* ... */ )
supportFragmentManager.setFragmentResult("event", result)
}
}
และถ้าเป็น Fragment ก็ต้องดูว่าจะใช้กับ FragmentManager ที่เป็น Parent หรือ Child
ถ้าใช้กับ Parent ก็จะมี Extension Function ให้ใช้ สามารถเรียกคำสั่ง setFragmentResult(...)
ได้โดยตรง แต่ถ้าเป็น Child ก็จะต้องเรียกผ่าน childFragmentManager
เสมอ
// MainFragment.kt
class MainFragment: Fragment() {
private fun sendEventToParentFragmentManager() {
val result = bundleOf( /* ... */ )
setFragmentResult("event", result)
}
private fun sendEventToChildFragmentManager() {
val result = bundleOf( /* ... */ )
childFragmentManager.setFragmentResult("event", result)
}
}
สิ่งที่ควรรู้เกี่ยวกับ Fragment Result API
Lifecycle-Aware
นั่นหมายความว่าข้อมูลที่ Publisher ส่งให้ FragmentManager จะถูกเก็บไว้จนกว่า Subsciber จะอยู่ในสถานะพร้อมทำงาน ดังนั้นไม่ต้องกลัวว่าข้อมูลจะถูกส่งให้ในตอนที่ยังไม่พร้อมทำงาน หรือข้อมูลหายกลางทาง (เว้นแต่ว่า FragmentManager จะถูกทำลายซะก่อน)
โดยสถานะพร้อมทำงานที่ว่าคือในช่วงเวลาหลังจาก onStart
ไปจนถึงก่อน onStop
ของ Activity หรือ Fragment ดังนั้นการใช้คำสั่ง setFragmentResultListner(...)
ก่อน onStart
จะยังไม่ได้ข้อมูลในทันที แต่จะได้ข้อมูลหลังจากที่ onStart
ทำงานเสร็จเรียบร้อยแล้ว
1 Request Key 1 Subscriber
ไม่สามารถกำหนด Request Key ให้ Component หลายตัวที่อยู่ใน FragmentManager เดียวกันได้ โดยจะมีผลแค่ Component ตัวสุดท้ายที่เรียกคำสั่ง setFragmentResultListener(...)
เท่านั้น จึงใช้งานแบบ Broadcast ไม่ได้
ใช้กับ Fragment ที่อยู่ใน ViewPager ก็ได้
เนื่องจากการสร้าง ViewPager สำหรับ Fragment จะต้องกำหนด FragmentManager อยู่แล้ว จึงสามารถใช้ Fragment Result API เพื่อส่งข้อมูลระหว่าง Fragment ที่อยู่ในนั้นหรือส่งให้ Fragment ของ ViewPager ตัวนั้น ๆ ได้เช่นกัน
และข้อมูลก็จะถูกส่งให้ Subscriber เมื่อพร้อมใช้งานตามเงื่อนไขของ Lifecycle-Aware นั่นเอง
Fragment Result API vs Shared ViewModel
ถ้าดูจากจุดประสงค์ในการใช้งานจะพบว่า Fragment Result API นั้นมีจุดประสงค์เหมือนกับ Shared ViewModel เลย แต่ทว่าทั้ง 2 วิธีก็มีข้อดีข้อเสียที่แตกต่างกันออกไป
ข้อดีของการใช้ Fragment Result API
- เป็น Lifecycle-Aware Components
- เหมาะกับข้อมูลแบบ One-Time Value เท่านั้น
- Request Key แต่ละตัวจะมี Subscriber ได้เพียงแค่ 1 ตัว
- ส่งข้อมูลให้ Component อื่น ๆ ที่อยู่ใน FragmentManager เดียวกัน
- มีโค้ดที่สั้นและกระชับ เหมาะกับการทำงานที่ไม่ซับซ้อนมากนัก
- ข้อมูลหายไปพร้อมกับ FragmentManager
- ระวังสับสน FragmentManager ระหว่าง Parent กับ Child
ข้อดีของการใช้ Shared ViewModel
- เป็น Lifecycle-Aware Components
- ข้อมูลเก็บไว้ใน LiveData จึงสามารถใช้คุณสมบัติของ LiveData ได้เต็มที่ รวมไปถึงเทคนิค Single Live Event เพื่อทำให้ LiveData ส่งข้อมูลแบบ One-Time Value
- LiveData แต่ละตัวจะมี Observer ได้มากกว่า 1 ตัว
- ส่งข้อมูลให้ Component ใด ๆ ก็ได้ที่ใช้ ViewModel ร่วมกัน
- สามารถเขียนโค้ดที่มีการทำงานซับซ้อนวางส่วนไว้ใน ViewModel ได้
- ข้อมูลหายไปพร้อมกับ ViewModel
สรุป
Fragment Result API ก็เป็นอีกหนึ่งทางเลือกสำหรับการส่งข้อมูลข้าม Component ในรูปแบบของ Publish-Subscribe Pattern ไม่ว่าจะเป็นการส่งข้อมูลระหว่าง Activity กับ Fragment หรือการส่งข้อมูลระหว่าง Fragment กับ Fragment ก็ตาม โดยใช้ประโยชน์จากการทำงานของ FragmentManager ที่คอยควบคุมการทำงานของ Fragment
การใช้งาน Fragment Result API จะเหมาะกับบางสถานการณ์เท่านั้น โดยเฉพาะการส่งข้อมูลง่าย ๆ ที่มีการทำงานไม่ซับซ้อนมากนัก ถ้านอกเหนือจากนั้นการใช้ Shared ViewModel ก็จะเป็นทางเลือกที่ดีกว่า