สับเปลี่ยนกระบวนท่าจาก MVP ไป MVVM และ Coroutine ยังไงไม่ให้แอพพัง
ในเมื่อหลายๆที่เขาใช้ MVVM กันแล้ว และทีมอื่นก็ด้วย ทำไมเรายังไม่มุ้ปอรหล่ะ!
ไปเจอโค้ดทีมนึงมา เป็น submodule แล้วมาเปลี่ยนเป็น endpoint เรา เจอ error พวก 404 not found, 401 unauthorized แล้วแอพแคลชเลย เฮ้ยยย มันต้อง handle ไว้ดิ
จากนั้นก็เลยศึกษา structure มาพอประมาณ จนมาลองเองในแอพบล็อกของตัวเอง จนได้วิธีที่เราสามารถเปลี่ยนจาก MVP ไป MVVM ได้แล้วหล่ะ
ปล. ตอนแรกกะจะมาสรุปบล็อกงานนี้ แต่ฟังกี่ทีก็จะงงๆ เลยแปะคลิปไว้ในนี้ให้คนอ่านตามฟังเองดีกว่า มันเหน่ยยยยยย เพราะสุดท้ายก็ต้องมาทำบล็อกอธิบายเพิ่มอยู่ดี เลยทำไปทีเดียวแล้วกันเนาะ
อีก ปล. บล็อกนี้เขียนนานมากหลายเดือนเพราะลองไปทำจริงในงานด้วย มันก็ต้องค่อยเป็นค่อยไปเนอะ ถ้าถามนานว่าขนาดไหน ก็ดูรูป cover แล้วกันเนอะ ช่วง WFH อ่ะ
ก่อนอื่นมารู้จัก Android Architecture Components กันก่อน
แน่นอนว่าเราเคยเขียนบล็อกไว้ แต่ได้แต่เขียนไง ยังไม่ได้เอาไปทำจริง ก็เลยมีหลงๆลืมๆไปบ้าง
เลยจะมาทบทวนกันสั้นๆสักนิดเนอะ
Architecture Components เป็น best practice ในการพัฒนา Android Application (ซึ่งหมายถึงเราจะทำตามหรือไม่ทำตามก็ได้ แต่ทำเถอะจะได้เป็นมาตรฐานเดียวกันงี้) ปัจจุบันอยู่ใน Android Jetpack อยู่ในส่วน Architecture นั่นเอง
แน่นอนว่าบล็อกเมื่อสามปีที่แล้วยังเป็น Java อยู่ แต่ประเด็นสำคัญจริงๆคือ ก่อนหน้านี้อ่ะ มันเป็น library แยกของตัวเอง ยังไม่ได้มี Jetpack ด้วยนะในตอนนั้น
มาทำความรู้จักแต่ละตัวกันเถอะ ที่จะพูดถึงกันในบล็อกนี้
- Data Binding มันก็จะมีบางคนที่คิดว่า MVVM ก็คือ data binding จริงๆมันคือ subset ของ MVVM เท่านั้น แค่ภาพจำยางช้าดเจนนนนน เฉยๆ (แนวเดียวกับคนที่รู้จัก BNK48 ก็ต้องรู้จักเฌอปรางอ่ะ)
ตัวนี้จะทำการ bind observable data ไปที่ view บางตัว เช่น TextView นี้ชื่อว่าtextStat
จะทำการ observe ว่าถ้ามีค่าของ stat เพิ่มมากก็อัพเดตเพิ่มไปใน View นี้ได้เลย - Lifecycles จัดการ lifecycle ของ Activity และ Fragment ช่วยแก้เรื่อง แก้ปัญหาเรื่อง Configuration change แล้วข้อมูลต้องโหลดใหม่ เลยมี
ViewModel
มาช่วยให้ทำงานได้อย่างต่อเนื่อง ส่วนView
ก็อยู่เฉยๆ คอย Observe แล้วก็นำข้อมูลจากในViewModel
มาแสดง - LiveData ทำหน้าที่ observe การเปลี่ยนแปลงของข้อมูลระหว่างหลายๆ components และส่ง update ไปยัง
Activity
หรือLifecycleOwner
ที่ active - Room คอนเซปคือ คือการเก็บ data หรือ cache ใน local พอ offline ก็เก็บ data แล้วส่งตอน online ทีหลัง ทำให้เราไม่ต้องโหลดข้อมูลจาก API ทุกครั้ง โดยใช้ database บน SQLite
สาเหตุที่ใช้ได้เหมือนกับการเรียก API เพราะว่าใช้ DAO (Data Access object) เหมือนกัน ซึ่งก็น่าจะเป็นพวก data class ที่มีการ declared functionequals()
/hashCode()
toString()
copy()
และcomponentN() functions
โดยที่เราไม่ต้องทำอะไรเลย
- ViewModel จัดการระหว่าง UI กับข้อมูลที่อยู่ใน lifecycle โดยที่เราไม่ต้องไปจัดการการอัพเดตข้อมูลที่ lifecycle เอง
- WorkManager เป็นตัวจัดการ background service ต่างๆ ซึ่งบล็อกนี้เราจะข้ามมันไปก่อนเนอะ
และการทำงานคร่าวๆมันจะเป็นแบบนี้
ก็คือตัว business logic ต่างๆจะไม่ผูกกับ View อีกต่อไป โดยตัว View ในที่นี้คือ Activity
หรือ Fragment
นั่นเอง มีหน้าที่ในการแสดงผลต่างๆ โดย observe data ต่างๆจาก LiveData
ที่มีอยู่ใน ViewModel
ซึ่งใน ViewModel
จะมี LiveData
กี่ตัวก็ได้
และเจ้า ViewModel
นี้จะทำการพูดคุยกับ Repository เพื่อดึงข้อมูลบางอย่างไปส่งให้เจ้า View ทั้งหลาย โดยการที่เราเรียก API ด้วย Retrofit นั้น จะอยู่ในส่วน Network นั่นเอง หรือหลายๆคนเรียกมันว่า Data Source แล้วอาจจะเก็บข้อมูลไว้ใน local ก็ได้ผ่านเจ้า Room
นั่นเอง แต่ตัวอย่างในบล็อกนี้ยังไม่ไปถึงห้องน้าาา~~
รู้จัก Coroutine กันสักหน่อย
หลังจากที่เราเข้าใจเรื่อง Architecture Components แล้ว มาต่อกันที่เรื่องของ Coroutine แบบสั้นๆกัน
Coroutine มาตอน Kotlin 1.3 เป็น concurrency design pattern ที่ใช้ใน Android ในการ executes พวก asynchronous มาช่วยแก้ปัญหา 2 อย่างด้วยกัน คือ
Manage long-running tasks
จัดการงานที่ทำมาอย่างยาวนาน ซึ่งทำให้ main thread ถูกล็อก และทำให้แอพเราค้าง
ในการสร้าง function ปกติ จะมี invoke
หรือ call
แล้วก็ return
เนอะ ใน Coroutine จะมีเพิ่มมา 2 ตัวด้วยกัน คือ
suspend
ถ้าไปหาคำแปลมามันจะแปลว่าแขวนเนอะ การทำงานของมันก็ตรงตัวคือ หยุดการ execute ชั่วคราวสำหรับ current Coroutineresume
ให้เริ่มทำงานต่อหลังจากที่ถูก suspend ไว้
การเรียกใช้ suspend
นั้น จะเรียกใช้เฉพาะ function ที่มี suspend ไว้ด้านหน้าเท่านั้น หรือสามารถสร้าง Coroutine ใหม่ ได้ดังนี้
launch
สร้างใหม่ ไม่ต้อง return อะไรกลับมา move on ได้ทันที เหมาะสำหรับ Regular Functionasync
สร้างใหม่ และให้ return อะไรสักอย่างกลับไปด้วย มักใช้คู่กับawait
โดยการใช้งานคร่าวๆนั้น เราใช้ async หน้า Unit และ await ตามหลังตัวแปร ถ้ามีหลายๆตัวสามารถมัดรวมกันเป็น list แล้วต่อท้ายด้วยawaitAll
ได้
Use Coroutine for main-safety
ใช้เพื่อความปลอดภัยหลัก ในการเรียก network และ disk operations จาก main thread
ใน Kotlin จะ require เจ้า dispatchers มาให้ 3 ตัวด้วยกัน
- Dispatchers.Main : ใช้ dispatcher ในการ run main thread เช่น เรียก
suspend
เพื่อ Android UI framework และ updateLiveData
- Dispatchers.IO : เกี่ยวกับ disk หรือ network ต่างๆ หรือการทำงานอื่นๆที่อยู่นอก main thread เช่น เรียก API ให้อยู่เบื้องหลัง, ให้
Room
อ่าน data - Dispatchers.Default : อันนี้ทำงานนอก main thread เหมือนกัน เช่น ให้ทำการเรียงข้อมูล และแปลงเป็นเจ้าก้อน JSON
withContext()
ใช้ในการสลับ thread ได้ครั้งนึง
CoroutineScope มักใช้กับ ViewModel
ใน Android Architecture Components
version ของ Retrofit ที่รองรับ suspend คือ version 2.6.0 นั่นเอง
จริงๆเรื่อง Coroutine ก็มี codelab ให้ลองทำด้วยนะ
เขาแนะนำให้ใช้กับ Architecture Components ที่มี ViewModel
, LiveData
, Repository
, and Room
การทำงานของ Thread ก็จะมี main thread และใช้ suspend
เพื่อแยกตัวออกไปทำงาน แล้ว resume กลับมา main thread อีกทีนึง
ถ้าอยากอ่าน Coroutine ภาษาไทยแบบไวๆสามารถอ่านได้ที่นี่เลย
มาปรับเปลี่ยนกระบวนท่ากันเป็น MVVM กันเถอะ
เมื่อเราเข้าใจทุกอย่างแล้ว มาลงมือทำกันดีกว่าเนอะ ตัวอย่างในบล็อกนี้คือแอพอ่านบล็อกของเรานั่นแหละ เราใช้ Ghost API เพื่อดึงบล็อกของเราขึ้นมาแสดง สามารถเข้าไปเรื่อง Ghost API รวมถึงการ redesign ใหม่ได้ที่นี่เลย
มาเริ่มทำกันเลย!~
ก่อนอื่นเพิ่ม dependency ต่างๆที่ต้องใช้กันก่อนนะ (เลข version ก็ใส่ล่าสุดที่ stable แล้วไปเนอะ คนอ่านอาจจะต้องไปอัพเดต version เอง 555)
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
ต่อมาประกาศพวก API ต่างๆ จาก Gateway สู่ Network โดยใช้ suspend
และไม่ต้อง return Call<T>
อีกต่อไป
ปล. ใน gist มันเรียงชื่อไฟล์ตามตัวอักษรหง่ะ ของเก่าคือ ApiService.kt
และของใหม่คือ ApiNetwork.kt
เรียกใช้ API ต่างๆที่ Repository
และใส่ Coroutine ลงไปด้วย จะคล้ายๆ class presenter แหละ แบ่งเป็นส่วน interface และ implement ซึ่งจะเอาไปใส่ใน Koin ต่อไป
เอา response data ไปใส่ใน ViewModel
และใส่ Coroutine ลงไปด้วย อย่าลืม handle ตอน error ด้วยนะ จริงๆจะมีประมาณ 2 แบบหลักๆ (ยังไม่พูดถึง UseCase นะ)
แบบแรก คือ ใส่ CoroutineExceptionHandler
เข้าไป เป็น parameter หลัง launch
หรือ async
ถ้าเราเรียก API นั้นไม่สำเร็จด้วยสาเหตุใดก็ตาม จะไปทำใน exception handler ตัวนี้ โดยตอนแรกที่เราเจอ crash จากอีกทีม หรือไม่ได้ handle error ตรงนี้อ่ะ
หรืออีกแบบก็คือแบบบ้านๆสามัญธรรมดา คือใส่ try-catch
เข้าไปนั่นเอง
สามารถดูตามในนี้ได้เลยเนอะ
ค่าต่างๆที่ได้จาก API นั้น มักจะเก็บไว้ใน LiveData
และทำการนำไปใช้ต่อ โดยผ่านเจ้า ViewModel
ที่เรา observe LiveData
อีกทีนี่แหละ
แต่ๆ อ่านหัวข้อต่อไปดีกว่าเนอะ
Dependency Injection ของมันต้องมี
อันโน้นก็น่ารัก อันนี้ก็น่ารัก โค้ดที่น่ารักก็น่าจะเป็นโค้ดที่ดูไม่ซับซ้อน ไม่สับสนเหมือนหลงไปในใจเธอแล้วถอนตัวออกไม่ได้ ที่สำคัญต้องเทสได้ด้วยนะ
Dependency Injection นั้น เราใช้เพื่อทำให้โค้ดของเรามีความอิสระต่อกัน ทำให้จัดการ และ test ได้ง่ายขึ้น มักจะเอาไปใช้กับ MVVM และ Coroutine ด้วย สามารถอ่านเพิ่มได้ที่นี่เนอะ อ่านแล้วไม่บาป ได้แต้มบุญเพิ่มแน่นอนน
สำหรับ Document ของ Koin สามารถเข้าไปอ่านได้ที่นี่
และแน่นอนคือ Koin นั้น ไม่ใช่ Dependency Injection นางเป็นเพียง Service Locator ที่ทำหน้าที่เหมือน Dependency Injection อ่ะ ถ้าของใหม่กว่านั้นก็คือ Hilt ที่แก้ pain จากความใช้ยากของ Dagger2 โดยการครอบ Dagger2 อีกที…
ไหนๆพูดถึง Hilt แล้วขอแปะวิดีโอไว้หน่อยเนอะ
เราจะมาอธิบายสั้นๆเกี่ยวกับการใช้ Koin ในที่นี้กันเนอะ
ก่อนอื่นเรามาเพิ่ม dependency ของเจ้า Koin กันก่อน
implementation "org.koin:koin-android-viewmodel:2.1.5"
implementation "org.koin:koin-androidx-ext:2.1.5"
จากนั้นเข้าไป init ที่ Application
class กันก่อน
androidLogger()
ใช้ AndroidLogger เป็น Koin Logger เอาไว้ log นั่นแหละandroidContext(this)
ใช้ context เฉพาะในแอพเราเท่านั้น โดยจะรับ context ในระดับApplication
เลยจ้าmodule
อันนี้อาจจะมีความจุกจิกบ้าง เป็น list ของ module ที่เราต้องการใช้ DI ในที่นี้จะแบ่งเป็น 2 ส่วน ดังนี้
1)networkModule
เอาไว้เพื่อกำหนด service ที่เราต้องใช้
2)blogModule
เอาไว้เพื่อ declare เจ้าRepository
และViewModel
ที่เราต้องการใช้
เรามักจะใช้เจ้า Koin เพื่อเอา ViewModel
ของเราไปใช้ในส่วนของ View นั่นเอง การเรียกใช้ก็แสนจะง่าย เป็นท่า Lazy Inject ViewModel
private val blogViewModel: BlogViewModel by viewModel()
ส่วนอีกท่านึง เรียกแบบตรงๆไปเลย
val blogViewModel : BlogViewModel = getViewModel()
จากนั้นก็เอาเจ้า ViewModel
ที่ได้ ไปใช้งานต่อไป
ในเมื่อ ViewModel เป็นที่ที่มี Logic เยอะที่สุด ดังนั้นก็ต้องใช้ LiveData ในการ Observe สิ
แต่โค้ดมันดูแปลกๆเนอะ สำหรับคนที่ใช้ MVVM แล้วมาอ่านบล็อกนี้คงอิหยังวะ เพราะ เรายังไม่ได้พูดถึงสิ่งเรียกว่า LiveData
นั่นเอง โดยใน ViewModel
1 ตัว สามารถมีได้หลาย LiveData
และการนำค่าต่างๆไปใช้ใน View
ก็จะทำการ observer เจ้าก้อน LiveData
นั่นเอง
ก่อนอื่นเอาแบบง่ายๆ เอา list blog ที่ได้ไปเป็น LiveData ซะ ขอแปะโค้ดเต็มก่อนแล้วค่อยอธิบายเนอะ
ใน class ของ ViewModel
จริงๆก็จะมีได้หลาย LiveData
เนอะ โดยในที่นี้จะเป็นการดึงบล็อกทั้งหมดหรือตาม hashtag จาก Ghost API มาแสดง โดยจะรับตัว response result เป็น MutableLiveData<T>()
ในกรณีที่มัน success จะนำค่า _allBlogPost
ไป set .value
เป็น response ตัวที่เราต้องการ อย่างในที่นี้ก็จะเป็น post ทั้งหมดเนอะ
แต่ถ้ามันไม่ success แล้วเราไม่ได้ดัก CoroutineExceptionHandler
แอพก็พังเนอะตามที่กล่าวไป และถ้าดักแล้ว เราสามารถ assign ค่าบางอย่าง เช่น ถ้ามัน error ให้แสดง UI ชุดนี้ขึ้นมานะ จึง assign เป็น Unit
แบบนี้ _getBlogError.value = Unit
ด้วยความที่เราเปลี่ยนให้มันเป็น MutableList ของ BlogPost ดังนั้นตัว function เราจะเปลี่ยนชื่อ function จาก onGetPostSuccess
ไปเป็น showBlogContent
แทน
การนำเจ้า LiveData
ไปเรียกใช้นั้น การเรียก service ต่างๆ ถ้าใน Fragment จะเรามักจะให้เรียกใช้ที่ onActivityCreated()
เพราะให้เรียก API ตอนที่ Activity
นั้นสร้างเสร็จ (onCreate()
) เลยโดยเราสร้าง function ที่ชื่อว่า loadPostData()
เพื่อเอาไปใช้งานในหลายๆที่ เช่น ดึงบล็อกทั้งหมดมาในตอนแรก ดึงบล็อกตาม hashtag ที่เลือกไว้
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
loadPostData(null)
}
private fun loadPostData(hashtag: String?) {
blogViewModel.getBlogPost(mPage, hashtag)
}
ความแตกต่างของonCreate()
,onCreateView()
และonActivityCreated()
ใน Fragment ต่างกันอย่างไร อ่าน top answer แล้วเข้าใจง่ายดีอ่ะ
และพวกการแสดงผลต่างๆ เราจะ Observer ค่าของ LiveData
จาก ViewModel
ที่ onViewCreated()
blogViewModel.allBlogPost.observe(viewLifecycleOwner, Observer {
showBlogContent(it)
})
ส่วนใน Activity นั้นมักจะเรียก API และ Observe LiveData
ที่ onCreate()
นะ ก็จะประมาณนี้แหละ
โค้ดทั้งหมดสามารถดูได้ที่นี่จ้า
เฮ้อออออเขียนเสร็จสักทีเนอะ
ส่วนการเทสต่างๆ ไปอ่านในนี้ได้เลยจ้า
ผลที่ได้ เราก็สามารถเรียกบล็อกทั้งหมดได้เหมือนเดิมแล้วนะ ที่เรามองไม่เห็นก็คือโครงสร้างโค้ดด้านในนั่นเอง แฮร่~~
ปล. คราวหน้าอาจจะพูดถึง Paging มั้งนะๆ
สุดท้าย เขียนบล็อกเสร็จแล้ว ฝากร้านได้ เย้ๆ