15 October 2018

ส่งข้อมูลระหว่าง Activity/Fragment แบบหล่อๆด้วย LiveData และ ViewModel ของ Android Architecture Components

Updated on


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

รูปแบบทั้งหมดที่เป็นไปได้สำหรับ Activity และ Fragment

         สำหรับการส่งข้อมูลระหว่าง Component ที่ชื่อว่า Activity กับ Fragment จะมีรูปแบบการส่งข้อมูลดังนี้

        • ส่งข้อมูลจาก Activity ไปให้ Activity
        • ส่งข้อมูลจาก Activity ไปให้ Fragment
        • ส่งข้อมูลจาก Fragment ไปให้ Activity
        • ส่งข้อมูลจาก Fragment ไปให้ Fragment


"ส่งข้อมูลจาก Activity ไปให้ Activity" ให้ใช้ Intent ได้เลย

        เนื่องจากแอนดรอยด์ออกแบบสิ่งที่เรียกว่า Intent ไว้ให้แล้ว เพื่อเป็นตัวกลางในการส่งข้อมูลไปมาระหว่าง Component พื้นฐานของแอนดรอยด์ (Activity, Service, Broadcast Receiver และ Content Provider) ดังนั้นจึงไม่ต้องคิดอะไรเยอะ

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

ปัญหาที่แท้จริงอยู่ที่ 3 รูปแบบที่เหลืออยู่

        เมื่อนักพัฒนาต้องการจะส่งข้อมูลจาก Fragment หรือส่งไปให้ Fragment ก็จะมีคำถามผุดขึ้นมาในใจว่า "ต้องทำยังไง?"

        กรณีของ "ส่งข้อมูลจาก Activity ไปให้ Fragment" ผู้ที่หลงเข้ามาอ่านสามารถส่งข้อมูลในตอนที่สร้าง Fragment ตัวนั้นๆขึ้นมาได้เลยแบบนี้

// SampleFragment.kt

class SampleFragment : Fragment() {
    private var key: String? = null
    private var position: Int = -1

    companion object {
        private const val EXTRA_KEY = "com.akexorcist.singleliveevent.sample.key"
        private const val EXTRA_POSITION = "com.akexorcist.singleliveevent.sample.position"

        fun newInstance(key: String?, position: Int): SampleFragment {
            val fragment = SampleFragment()
            val bundle = Bundle()
            bundle.putString(EXTRA_KEY, key)
            bundle.putInt(EXTRA_POSITION, position)
            fragment.arguments = bundle
            return fragment
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        savedInstanceState?.let { state ->
            key = state.getString(EXTRA_KEY)
            position = state.getInt(EXTRA_POSITION, -1)
        }
    }
}

        โดยสร้าง Fragment ขึ้นมาและแนบข้อมูลที่ต้องการเข้าไปใน Bundle และเวลาที่ Fragment ทำงานตาม Lifecycle ก็สามารถดึงข้อมูลออกมาจาก Bundle ที่ส่งเข้ามาใน onCreate(...), onCreateView(...), onViewCreated(...) หรือ onActivityCreated(...) ได้เลย

         และ Activity ก็ส่งข้อมูลที่ต้องการเข้าไปแบบนี้

// MainActivity.kt

class MainActivity : AppCompatActivity() {
    ...
    override fun doSomething() {
        val key = ...
        val position = ...
        supportFragmentManager.beginTransaction()
                .add(..., SampleFragment.newInstance(key, position))
                ...
    }
}

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


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


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

แก้ปัญหาด้วย Event Bus

        Event Bus นั้นเป็นวิธีที่จะเข้ามาช่วยแก้ปัญหานี้ได้เป็นอย่างดี เพราะเป็นรูปแบบการทำงานแบบ Publisher กับ Subscriber โดยที่ทั้งสองฝั่งไม่จำเป็นต้องรู้จักกัน ฝั่ง Publisher ไม่จำเป็นต้องรู้ว่า Subscriber คือใคร และ Subscriber ก็ไม่ต้องรู้เช่นกัน จึงทำให้ Event Bus เป็นตัวช่วยในการส่งข้อมูลระหว่าง Component ที่มีความยืดหยุ่นสูงมาก

        ถ้านักพัฒนาอยากจะใช้ Event Bus บนแอนดรอยด์ก็จะมี Library ยอดนิยมให้เลือกระหว่าง 


แต่ Event Bus ก็ไม่ได้เป็นมิตรกับ Lifecycle และ ConfigurationChange

        ถ้าไม่มีเรื่อง Lifecycle ของ Fragment และ Configuration Changes บนแอนดรอยด์ การเลือกใช้ Event Bus ก็คงลงตัวอย่างสมบูรณ์ โดยจะเห็นได้ชัดเจตก็คือตอนที่อยากจะส่งข้อมูลบางอย่างจาก Fragment ตัวหนึ่งไปให้อีกตัวหนึ่งที่อยู่ใน ViewPager ตัวเดียวกัน และ Fragment ตัวนั้นดันอยู่ไกลเกินจนโดน ViewPager ทำลายทิ้งชั่วคราว 


           เมื่อส่งข้อมูลจาก Fragment ที่แสดงผลอยู่ไปให้ Fragment ที่ยังไม่ได้สร้างหรือว่าถูกทำลายทิ้งชั่วคราว ก็กลายเป็นว่า Data ก้อนนั้นหายไปกลางอากาศทันที ซึ่งไม่ใช่เรื่องดีแน่นอนถ้าเกิดเหตุการณ์แบบนี้ขึ้น

หยิบความสามารถ LiveData มาช่วยแก้ปัญหา

        เพื่อแก้ปัญหาจุกจิกอย่างเรื่อง Lifecycle หรือ Configuration Changes บนแอนดรอยด์ ทีม Google จึงสร้างสิ่งที่เรียกว่า Android Architecture Components ขึ้นมาเพื่อลดปัญหาเหล่านี้ โดยมีหัวใจสำคัญคือ LiveData และ ViewModel

        และถ้าลองดูใน googlesamples/android-architecture บน GitHub ก็จะพบว่าทีม Google ได้สร้างสิ่งที่เรียกว่า SingleLiveEvent ขึ้นมาโดยใช้ความสามารถของ LiveData 

// SingleLiveEvent.kt

class SingleLiveEvent<T> : MutableLiveData<T>() {
    private val mPending = AtomicBoolean(false)

    companion object {
        private val TAG = "SingleLiveEvent"
    }

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {

        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
        }

        // Observe the internal MutableLiveData
        super.observe(owner, Observer { t ->
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    @MainThread
    override fun setValue(t: T?) {
        mPending.set(true)
        super.setValue(t)
    }

    @MainThread
    fun call() {
        value = null
    }
}

        แล้วมันต่างจาก LiveData ปกติยังไงล่ะ?

        จาก Comment ในคลาสดังกล่าวได้เขียนอธิบายไว้ดังนี้

/**
 * A lifecycle-aware observable that sends only new updates after subscription, used for events like
 * navigation and Snackbar messages.
 *  * This avoids a common problem with events: on configuration change (like rotation) an update
 * can be emitted if the observer is active. This LiveData only calls the observable if there's an
 * explicit call to setValue() or call().
 * 

 * Note that only one observer is going to be notified of changes.
 */



        โดยปกติแล้วเวลา LiveData ถูกกำหนดค่าใดๆลงไป เมื่อถูก Observer ด้วย Activity หรือ Fragment ก็จะโยนค่านั้นๆออกมาให้ทุกครั้ง รวมไปถึงตอนที่เกิด Configuration Changes ด้วย เพราะปกติแล้ว LiveData ควรจะ Observe ไว้ตั้งแต่ Activity หรือ Fragment ถูกสร้างขึ้น เพื่อให้ทำงานตอนเกิด Configuration Changes ได้ด้วย ดังนั้น LiveData จึงโยนค่าออกมาทุกครั้งที่ค่ามีการเปลี่ยนแปลงหรือถูก Observe

        แต่ว่า SingleLiveEvent มีการใช้ AtomicBoolean เข้ามาช่วยเพื่อเช็คให้ LiveData ส่งข้อมูลให้ปลายทางแค่ครั้งเดียว ถ้ามีการกำหนค่าใดๆลงไปใน LiveData แล้วมีตัวไหนที่ Observe เป็นตัวแรกสุดก็จะส่งข้อมูลให้แค่ตัวนั้นเท่านั้น จึงเป็นที่มาของชื่อ SingleLiveEvent นั่นเอง

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

        ซึ่ง SingleLiveEvent จะส่งใช้คำสั่งเพื่อข้อมูลได้ 2 แบบดังนี้

// ส่งข้อมูลตามประเภทที่กำหนดไว้ใน Generic
setValue(...)

// ส่งข้อมูลเป็น null
call()

        แต่ทว่าคลาส SingleLiveEvent ไม่ใช่ Standard Class ของ Android Architecture Components ดังนั้นผู้ที่หลงเข้ามาอ่านจะต้องสร้างขึ้นมาใช้งานเองนะ

LiveData จะสมบูรณ์ได้ ก็ต้องมี ViewModel สิ

        จะให้เอา SingleLiveEvent ไปใช้งานตรงๆเลยก็คงไม่มีประโยชน์อะไรถ้าขาด ViewModel ไป เพราะ ViewModel เป็นสื่อกลางที่จะให้ Activity หรือ Fragment สามารถเรียกใช้ LiveData ได้อย่างมีประสิทธิภาพ เพราะ ViewModel จะคอยรักษา LiveData ไม่ให้ตายไปตาม Configuration Changes

        โดยแนะนำว่าควรแยก SingleLiveEvent เป็นหลายๆตัวสำหรับแต่ละ Event ไม่ควรเอาไป Reuse ใช้งานร่วมกันในหลายๆ Component เพราะจะทำให้คาดเดาไม่ได้ว่าข้อมูลไปโผล่อยู่ที่ไหน ดังนั้นถ้าระหว่าง Fragment กับ Activity มี 2 Event ที่อยากจะส่งระหว่างกัน ก็ให้สร้าง SingleLiveEvent ขึ้นมา 2 ตัว แล้วอยู่ใน ViewModel ตัวเดียวกัน

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

// MainActivityEventViewModel.kt

class MainActivityEventViewModel : ViewModel() {
    val changeFragmentEvent = SingleLiveEvent<Unit>()
    val updateUserNameEvent = SingleLiveEvent<String>()

    fun changeFragment() {
        changeFragmentEvent.call()
    }

    fun updateUserName(name: String?) {
        updateUserNameEvent.value = name
    }
}

        จากนั้นก็ให้ Binding ให้กับ Activity ตัวที่ต้องการซะ และให้ Observe ค่า changeFragmentEvent กับ updateUserNameEvent ให้เรียบร้อย

// MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var eventViewModel: MainActivityEventViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        eventViewModel = ViewModelProviders.of(this).get(MainActivityEventViewModel::class.java)
        eventViewModel.changeFragmentEvent.observe(this, Observer { data: Unit? ->
            // Change the fragment
        })
        eventViewModel.updateUserNameEvent.observe(this, Observer { data: String? ->
            // Update new user name 
        })
    }
}

        ถึงแม้ว่า Dependency ของ LiveData และ ViewModel จะถูกรวมไว้ใน AppCompat เวอร์ชันล่าสุดแล้ว แต่ในขั้นตอนการ Binding ที่จะต้องเรียกผ่านคลาสที่ชื่อว่า ViewModelProviders นั้นจะไม่ได้มีอยู่ใน AppCompat ดังนั้นผู้ที่หลงเข้ามาอ่านจะต้องเพิ่ม Dependency ดังนี้

// build.gradle

implementation 'android.arch.lifecycle:extensions:<latest_version>'

        เมื่อสร้าง SingleLiveEvent กับ ViewModel เสร็จแล้ว และ Binding + Observe ไว้ใน Activity เสร็จเรียบร้อยแล้ว ที่เหลือก็คือเรียกใช้งานผ่าน Fragment ที่ต้องการ

// ProfileFragment.kt

class ProfileFragment : Fragment() {
    private lateinit var mainActivityEventViewModel: MainActivityEventViewModel
    ...
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        activity?.let { activity ->
            mainActivityEventViewModel = ViewModelProviders.of(activity).get(MainActivityEventViewModel::class.java)
        }
    }

    private fun updateUserNameToMainActivity() {
        val name = ...
        mainActivityEventViewModel.updateUserName(name)
    }
}

        หัวใจสำคัญของการ Binding บน Fragment คือจะต้องโยนคลาส Activity เข้าไปเท่านั้นนะ เพื่อให้ ViewModel ของ Activity และ Fragment เป็นตัวเดียวกัน จึงเป็นที่มาว่าทำไมเจ้าของบล็อกจึงใช้คำสั่ง Binding ใน onActivityCreated(...) นั่นเอง

        เพียงเท่านั้น ฝั่ง Fragment ก็สามารถส่งข้อมูลให้ Activity หรือสั่งให้ทำงานบางอย่างได้โดยไม่ต้องกังวลเรื่อง Lifecycle หรือ Configuration Changes อีกต่อไป


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

สรุป

        SingleLiveEvent เป็นการนำ LiveData มาประยุกต์ใช้งานเพื่อให้ส่งข้อมูลเพียงแค่ครั้งเดียว โดยจะส่งให้กับ Component ตัวแรกสุดที่ทำการ Observe ส่วนตัวใดๆที่ Observe หลังจากนั้นก็จะไม่ได้รับข้อมูล ซึ่งมีข้อดีที่นักพัฒนาไม่ต้องไปยุ่งกับ Lifecycle กับ Configuration Changes เลย เพราะว่า LiveData และ ViewModel คอยจัดการให้หมดแล้ว

        ถึงแม้ว่าการใช้ Event Bus จะมีจำนวนโค้ดที่น้อยกว่าการทำ SingleLiveEvent + ViewModel แบบนี้ แต่เอาเข้าจริงแล้ว ถ้าผู้ที่หลงเข้ามาอ่านใช้ Event Bus อยู่ และจะต้องรองรับ Lifecycle ของ Activity กับ Fragment และรองรับ Configuration Changes ได้ สุดท้ายก็ต้องเขียนโค้ดเพิ่มเข้าไปอยู่ดี เผลอๆโค้ดเหล่านั้นจะทำให้โค้ดรกและเขียนเยอะกว่านี้เสียอีก

        ดังนั้นการเอา LiveData กับ ViewModel มาประยุกต์ใช้งานเพื่อทำเป็นตัวรับ/ส่งข้อมูล "จาก Activity ไปให้ Fragment", "จาก Fragment ไปให้ Activity" หรือ "จาก Fragment ไปให้ Fragment" ก็เป็นไอเดียที่ดีกว่าถึงแม้ว่าจะต้องสร้าง ViewModel ให้กับแต่ละ Component ทุกครั้ง เพื่อไม่ให้ Event ปนกันเมื่อต้องเขียนโค้ดอีกมากมายเพิ่มเข้ามาในอนาคต