หลังจากที่ใช้งาน Fragment กันได้แล้ว คราวนี้ก็จะมาถึงคำถามยอดนิยมสำหรับผู้ที่หลงเข้ามาอ่านที่ใช้งาน Fragment นั่นก็คือการรับส่งข้อมูลของ Fragment ไม่ว่าจะเป็นระหว่าง Activity ←→ Fragment หรือ Fragment ←→ Fragment ก็ตาม

บทความในซีรีย์เดียวกัน

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

สำหรับการรับส่งข้อมูลของ Fragment นั้นทำได้หลากหลายวิธี ขึ้นอยู่กับความเหมาะสมและรูปแบบในการทำงานของ Fragment

  • ส่งข้อมูลด้วย Constructor Arguments (Activity → Fragment)
  • ส่งข้อมูลผ่าน Event Listener (Fragment → Activity)
  • ส่งข้อมูลด้วย Direct Method Call (Activity → Fragment)
  • ส่งข้อมูลด้วย Event Bus (Activity/Fragment ←→ Fragment)
  • ส่งข้อมูลด้วย ViewModel (Activity/Fragment ←→ Fragment) - แนะนำวิธีนี้

โดยแต่ละแบบจะมีรูปแบบในการใช้งานดังนี้

ส่งข้อมูลด้วย Constructor Arguments

Activity → Fragment

ชื่อหัวข้ออาจจะไม่คุ้นหู แต่เป็นวิธีที่ใช้กันบ่อย ซึ่งเป็นการกำหนดให้ Activity ส่งข้อมูลเป็นแบบ Bundle ไปให้ Fragment ตั้งแต่ตอนสร้าง Fragment ขึ้นมานั่นเอง

// Fragment
class AwesomeFragment : Fragment() {
    private var message: String? = null
    private var number = 0
    
    companion object {
        const val EXTRA_MESSAGE = "extra_message"
        const val EXTRA_NUMBER = "extra_number"
        
        fun newInstance(message: String?, number: Int): AwesomeFragment {
            return AwesomeFragment().apply {
                arguments = Bundle().apply {
                    putString(EXTRA_MESSAGE, message)
                    putInt(EXTRA_NUMBER, number)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let { bundle: Bundle ->
            message = bundle.getString(EXTRA_MESSAGE)
            number = bundle.getInt(EXTRA_NUMBER)
        }
    }

    /* ... */
}

และ Activity ก็สามารถแนบข้อมูลที่ต้องการผ่านคำสั่ง newInstance(...) ที่เตรียมไว้ใน Fragment

// Activity
class AwesomeActivity : AppCompatActivity() {
    /* ... */
    private fun openAwesomeFragment() {
        supportFragmentManager.commit {
            val fragment: AwesomeFragment = AwesomeFragment.newInstance(
                message = "Secret Password",
                number = 1234
            )
            replace(R.id.layoutFragmentContainer, fragment)
        }
    }
}
คำสั่ง commit { } ที่เจ้าของบล็อกใช้เป็น Extension Function ของ KTX Fragment เพื่อช่วยให้คำสั่งกระชับมากขึ้น ส่วน R.id.layoutFragmentContainer เป็น ViewGroup เพื่อใช้เป็นพื้นที่ในการแสดง Fragment ท่ีได้สร้างขึ้นมา

เมื่อ Fragment ถูกสร้างขึ้นมาและเริ่มทำงาน ก็จะดึงข้อมูลจาก arguments เพื่อนำไปใช้งานนั่นเอง

โดยวิธีนี้มีไว้ใช้ในกรณีที่ Activity ต้องการส่งข้อมูลให้ Fragment แค่ตอนที่ถูกสร้างขึ้นเท่านั้น แต่ถ้าเป็น Fragment ที่กำลังทำงานอยู่ (ถูกสร้างขึ้นมาเรียบร้อยแล้ว) จะไม่สามารถใช้วิธีแบบนี้ได้

ส่งข้อมูลผ่าน Event Listener

Fragment → Activity

เป็นการสร้าง Event Listener เพื่อให้ Fragment เรียกใช้งาน เวลาที่ต้องการส่งค่าระหว่างกัน ยกตัวอย่างเช่น อยากจะให้ Fragment ส่งข้อมูลไปหา Activity ได้ จึงสร้าง Event Listener ขึ้นมาแบบนี้

// AwesomeFragmentListener.kt
interface AwesomeFragmentListener {
    fun onButtonOkClick()
    fun onButtonCloseClick()
    fun onLoginSuccess(data: UserData?)
}

และที่ฝั่ง Activity ก็ให้ประกาศ AwesomeFragment.Listener ในรูปแบบของการ Implement เตรียมไว้แบบนี้

// Activity
class AwesomeActivity : AppCompatActivity(), AwesomeFragmentListener {
    /* ... */

    override fun onOkClick() { }

    override fun onCloseClick() { }

    override fun onLoginSuccess(data: UserData?) { }
}

และเวลาที่ Fragment ต้องการส่งข้อมูลไปให้ Activity ก็ให้เรียกผ่าน Event Listener ตัวนั้นแทน

// Fragment
class AwesomeFragment : Fragment() {
    /* ... */

    private fun sendOkButtonClickEvent() {
        (activity as? AwesomeFragmentListener)?.onOkClick()
    }
}

ในกรณีที่ Activity ไม่ได้ประกาศไว้ตั้งแต่แรก คำสั่งดังกล่าวก็จะไม่มีผลอะไร

จะเห็นว่าวิธีนี้จะช่วยให้นักพัฒนานำ Fragment ไปใช้งานกับ Activity ใดๆก็ได้ ไม่ต้องกังวลว่า Fragment จะผูกอยู่กับ Activity ตัวใดตัวหนึ่ง

ในทางกลับกัน ถ้าอยากจะส่งข้อมูลจาก Activity ไปให้ Fragment ก็สามารถใช้วิธีนี้ได้เช่นกัน (แต่ไม่ค่อยนิยมกันซักเท่าไร)

ส่งข้อมูลด้วย Direct Method Call

Activity → Fragment

เนื่องจาก Activity ทำหน้าที่สร้าง Fragment อยู่แล้ว จึงสามารถเก็บ Fragment ไว้เพื่อส่งข้อมูลในช่วงเวลาที่ต้องการได้เลย

// Activity
class AwesomeActivity: AppCompatActivity() {
    private lateinit val fragment: AwesomeFragment
    /* ... */
    private fun initAwesomeFragment() {
        supportFragmentManager.commit {
            val fragment: AwesomeFragment = AwesomeFragment.newInstance(
                message = "Secret Password",
                number = 1234
            )
            replace(R.id.layoutFragmentContainer, fragment)
        }
    }
    
    fun onInfoSaveClick() {
        /* ... */
        fragment.onUpdateData(/* ... */)
    }
}

// Fragment
class AwesomeFragment: Fragment() {
    /* ... */
    
    fun onUpdateData(/* ... */) {
        /* ... */
    }
}

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

ส่งข้อมูลด้วย Event Bus

Activity / Fragment ←→ Fragment

สำหรับวิธีนี้เป็นการใช้ Event Bus เข้ามาช่วย ซึ่งจะใช้ Library ตัวไหนก็ได้ เพราะใช้ Concept เดียวกัน แต่สำหรับตัวอย่างนี้จะใช้ Otto

ก่อนที่จะเริ่มส่งข้อมูลผ่าน Event Bus จะต้อง Register และ Unregister ให้กับ Activity หรือ Fragment ปลายทางที่จะรับข้อมูลผ่าน Event Bus เสียก่อน

// Activity
class AwesomeActivity : AppCompatActivity() {
    /* ... */
    override fun onStart() {
        /* ... */
        Bus.getInstance().register(this)
    }

    override fun onStop() {
        /* ... */
        Bus.getInstance().unregister(this)
    }
}

โดยจะใส่ไว้ใน onStart() และ onStop() นั่นเอง

จากนั้นก็เตรียม Method สำหรับรับข้อมูลจาก Event Bus โดยใช้ @Subscribe ซึ่งเป็น Annotation ของ Otto

// Activity
class AwesomeActivity : AppCompatActivity() {
    /* ... */
    @Subscribe
    fun onUserInfoUpdate(info: UserInfo) {
        // Do something when any event on fragment was happened
    }
}

และเมื่อ Fragment อยากส่งข้อมูลมาให้ Activity ตัวนี้ก็เพียงแค่ใช้คำสั่งแบบนี้

val info: UserInfo = /* ... */
Bus.getInstance().post(info)

ด้วย Concept ของ Event Bus จึงทำให้นักพัฒนาสามารถใช้ส่งข้อมูลระหว่าง Activity กับ Fragment ได้ตามใจชอบ หรือจะส่งระหว่าง Fragment กับ Fragment ที่อยู่ใน Activity ตัวเดียวกันก็ได้เช่นกัน

ในขณะเดียวกัน จุดที่นักพัฒนาต้องระวังในการใช้งาน Event Bus ก็คือความอิสระมากเกินไปของ Event Bus ที่สามารถส่งข้อมูลจากที่ไหน ไปที่ไหนก็ได้ ซึ่งถ้าใช้เยอะจนเกินไปก็อาจจะทำให้การรับส่งข้อมูลภายในแอปสามารถดูหรือตรวจสอบเวลาเกิดปัญหาได้ยาก

ส่งข้อมูลระหว่าง Fragment กับ Fragment

ที่ผ่านมาจะเน้นไปที่การรับส่งข้อมูลระหว่าง Activity กับ Fragment แต่ว่าในการทำงานจริงนั้น ไม่น้อยเลยที่นักพัฒนาจะต้องรับส่งข้อมูลระหว่าง Fragment กับ Fragment ที่อยู่ใน Activity ตัวเดียวกัน

ซึ่งกรณีแบบนี้จะทำได้หลายวิธี

สำหรับ Fragment ที่อยู่ใน Activity เดียวกัน

  • การใช้ Event Bus เพื่อส่งข้อมูลระหว่าง Fragment กับ Fragment โดยตรงเลย
  • Fragment ส่งข้อมูลให้ Activity ผ่าน Event Listener แล้วให้ Activity ส่งข้อมูลให้ Fragment ที่ต้องการด้วยการเรียก Method ของ Fragment นั้นๆโดยตรง

สำหรับ Fragment ที่อยู่ใน ViewPager เดียวกัน

  • ใช้ PagerAdapter เป็นตัวกลางเพื่อส่งข้อมูลจาก Fragment หนึ่งไปอีก Fragment หนึ่ง
  • ใช้ Event Bus ส่งข้ามหากันโดยตรง

ส่งข้อมูลผ่าน ViewModel ด้วย LiveData (แนะนำ)

Activity / Fragment ←→ Fragment

ViewModel และ LiveData เป็น Component ตัวหนึ่งใน Android Architecture Components ที่ทีมพัฒนาของ Android ที่ Google สร้างขึ้นมาเพื่อช่วยให้นักพัฒนาสามารถจัดการกับทำงานของแอปได้ง่ายขึ้น ซึ่งสามารถนำมาใช้งานเพื่อรับส่งข้อมูลระหว่าง Activity และ Fragment ได้อีกด้วย

โดยรายละเอียดของวิธีนี้เจ้าของบล็อกได้เขียนเป็นบทความแยกไว้แล้วใน ส่งข้อมูลระหว่าง Activity/Fragment แบบหล่อๆด้วย LiveData และ ViewModel ของ Android Architecture Components

ถ้าเป็นไปได้ก็แนะนำให้ใช้วิธีนี้ เพราะเป็นวิธีที่มีประสิทธิภาพ ไม่ทำลายโครงสร้างโค้ด และใช้งานได้ง่าย

สรุป

จะเห็นว่ารูปแบบในการรับส่งข้อมูลระหว่าง Activity กับ Fragment หรือ Fragment กับ Fragment นั้นมีหลายวิธีมาก ขึ้นอยู่กับทิศทางของข้อมูล จุดประสงค์ และช่วงเวลาในการทำงานของโค้ด

ถึงแม้ว่าเจ้าของบล็อกจะอธิบายรูปแบบต่างๆมากมาย แต่ส่วนหนึ่งก็เพื่อให้นักพัฒนาสามารถเข้าใจวิธีการใช้งานในรูปแบบต่างๆมากขึ้นเท่านั้น เพราะท้ายที่สุดแล้วก็อยากให้นักพัฒนาใช้ ViewModel + LiveData มากกว่า เพราะเป็นวิธีที่ค่อนข้างสะดวกและตอบโจทย์นักพัฒนามากที่สุดในตอนนี้