ตัวอย่างลองใช้งาน StateFlow APIs ใน Android

Theerapong.Kha
te<h @TDG
Published in
3 min readDec 9, 2020

ในบทความนี้ จะมาแนะนำการใช้งาน Kotlin Coroutine StateFlow ใน Android ซึ่งถูกเพิ่มเข้ามาใน Coroutines ตั้งแต่เวอร์ชั่น 1.4.0 ขึ้นไป จะมีลักษณะการใช้งานคล้ายกับ LiveData มาก แต่ว่ามีความแตกต่างยังไง มาลองชมตัวอย่างการใช้งานได้ในรายละเอียดต่อไปครับ

StateFlow คืออะไร และใช้งานยังไง?

StateFlow ถูกออกแบบมาเพื่อใช้งานกับ use case ที่มาการเปลี่ยนแปลงของ state หรือทำหน้าที่เป็นตัวจัดการ state ในแอปพลิเคชั่น อธิบายเพิ่มเติม คือ ในแอปพลิเคชั่นจะต้องมี use case ที่มีการเปลี่ยนแปลง state ต่าง ๆ เช่น เมื่อเราเรียกข้อมูลจาก API จะมี state ของการทำงาน ดังนี้ ขณะรอผลลัพธ์จาก API คือ Loading, โหลดข้อมูลสำเร็จ คือ Success, และหากมีข้อผิดพลาด คือ Error

การใช้งาน StateFlow คล้ายกับการใช้งาน LiveData คือ สร้างตัวแปรเป็นชนิด MutableStateFlow เพื่อทำหน้าที่เป็นตัว setter หรือเปลี่ยนแปลงค่า เหมือน LiveData ที่ต้องสร้างตัวแปรเป็น MutableLiveData แต่ StateFlow จะต้องกำหนดค่าเริ่มต้นของ state ใน constructor เสมอ ขณะที่ LiveData ไม่ต้องประกาศตัวแปรใน constructor

Step 1 สร้าง StateFlow

ตัวอย่างแอปที่จะทดสอบใช้งาน เพื่อให้เห็นภาพการนำ StateFlow ไปใช้ จะมีลักษณะการทำงานง่าย ๆ ดังนี้ มีหน้าจอ login ที่ user จะต้องป้อน Username และ Password

  • หาก user กดปุ่ม login เพื่อเข้าสู่ระบบ จะเข้าสู่ state ของการ Loading
  • ตรวจสอบการป้อน Username หรือ Password หากเป็นค่าว่าง จะกำหนด state เป็น InvalidInput
  • หากข้อมูล Username, Password มีค่า ทำการตรวจสอบว่าข้อมูลว่าถูกต้องหรือไม่ ถ้าถูกต้องกำหนด state เป็น Success
  • และกำหนด state เป็น Error เมื่อมีข้อผิดพลาด

จากรายละเอียดข้างต้น จะได้ไฟล์ MainViewModel ที่มี function login() ดังนี้

ตัวอย่างโค้ด MainViewModel

จากโค้ด มีการสร้าง sealed Class เพื่อกำหนด model ของข้อมูลว่ามี state ใดบ้าง เช่น Loading, InvalidInput, Success, Error, Empty

ทำการสร้างตัวแปรเพื่อเก็บ state ของการ login เป็นชนิด MutableStateFlow เพื่อเก็บค่า LoginUiState และต้องทำการกำหนด default value เข้าไปด้วยเสมอ จากตัวอย่าง คือ LoginUiState.Empty

ที่ฟังก์ชั่น login(userName:String, password:String) เราทำการตรวจสอบ เพื่อกำหนดแต่ละ state ตาม logic มีคำอธิบาย ดังนี้

  • Loading เมื่อเรียก function login
  • InvalidInput เมื่อไม่พบการป้อน Username หรือ Password
  • Success เมื่อ Username และ Password ตรงตามเงื่อนไข
  • Error เมื่อมีข้อผิดพลาด เช่น Username และ Password ไม่ตรงตามที่กำหนด

และสร้าง getter ชื่อ onLoginUiState() เพื่อนำไปเรียกใช้งานค่าใน MainActivity ต่อไป

Step 2 การเรียกใช้งานและรับค่าจาก StateFlow

ตัวอย่างโค้ดใน MainActivity ทำการ setup view, กำหนด listener ของ login Button และทำการ observe ข้อมูลจาก StateFlow ใน viewModel ดังนี้

ตัวอย่างโค้ด MainActivity
  • กำหนด scope การทำงานของ coroutine ว่าจะให้ทำงานเมื่อแอปอยู่ในสถานะขั้นต่ำ คือ Start และจะหยุดการทำงานเมื่อแอปอยู่ในสถานะ Stop โดยใช้ lifecycleScope.launchWhenStarted
  • จากนั้นทำการรอรับค่าของ loginUiState ด้วยการเรียก viewModel.onLoginUiState().collect {…}
  • ที่ฟังก์ชั่น fun renderOnLoginUiState(state: LoginUiState) คอยรับค่าที่เปลี่ยนแปลงจาก StateFlow และแสดงผล User Interface ตาม state เช่น แสดงผล ProgressBar เมื่ออยู่ใน state ของ Loading และซ่อน ProgressBar ไปเมื่ออยู่ใน state ของ Success, Error เป็นต้น
ตัวอย่างแอปทดลองใช้ StateFlow

สรุป

จากการทดลองใช้งาน StateFlow พบว่ามีการใช้งานแทบจะเหมือนกับการที่เราเรียกใช้ liveData แต่ว่ามีข้อแตกต่างกัน ดังนี้

  • StateFlow จะต้องกำหนดค่า state เริ่มต้นใน constructor ด้วยเสมอ
  • liveData.observe() จะทำการ unregister หรือหยุด observe ข้อมูลให้เราอัตโนมัติเมื่อแอปอยู่ในสถานะ stop หรือเมื่อวิวไม่แสดงผล ขณะที่ StateFlow ยังสามารถรับข้อมูลใน collect ได้อยู่ หากไม่ได้กำหนด lifecycleScope ขั้นต่ำว่า launchWhenCreate หรือ launchWhenResumed ทำให้หากมีการอัปเดต UI ในขณะที่วิวไม่แสดงผล จะเป็นการสิ้นเปลือง resource หรืออาจมี Crash ได้
  • เราสามารถแก้ปัญหาข้างต้นด้วยการแปลง StateFlow ให้เป็น liveData ด้วยการเรียกใช้ asLiveData() โดยการเพิ่ม library ชื่อว่า lifecycle-livedata-ktx
  • ด้วยลักษณะการทำงานของ SteteFlow คล้าย LiveData แต่ว่ามีความสามารถเพิ่มขึ้นมา คือ สามารถเรียกใช้ operator ต่าง ๆ ได้ด้วย เช่น map, filter, reduce, flatMapMerge เป็นต้น

ที่มา

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow

Source Code

--

--