ในการส่งข้อมูลระหว่าง 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 ก็จะเป็นทางเลือกที่ดีกว่า

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