รู้จัก Kotlin Coroutines ตั้งแต่ Zero จนเป็น Hero : ตอนที่ 3

ความเดิมตอนที่แล้ว

หลายๆคนก็ได้ทำความรู้จักกับคำศัพท์ที่เราจะได้พบเจอบ่อยๆ กับ Kotlin Coroutines และตัวอย่างกันใช้งานกันบ้างคร่าวๆ แล้วแต่ในตอนท้ายเราได้มีการทิ้งท้ายคำศัพท์อีกตัวไว้อีกเล็กน้อย สำหรับใครที่พลาดตอนแล้วไปคลิกข้างล่างโล้ดดด.. จิ้มมมม เบาๆ

เนื้อหาที่คุณจะได้เรียนรู้ในบทความนี้

  • คำศัพท์ที่เราจะได้พบเห็นเจอกันบ่อย ๆ
  • Dispatcher ในรูปแบบต่าง ๆ
  • Coroutine Dispatcher interface
  • Coroutine Context และ Coroutine Scope
  • Job และ Parent job
  • ตัวอย่างการใช้งานแบบจริงๆ จังๆ ขนมาหมดหน้าตักกันเลย

โดยในตอนนี้เราจะทำความรู้จักคำศัพท์กันต่ออีกนิดนึง แล้วจะเริ่มเขียนกันจริงๆ จังๆ กัน แต่ก่อนอื่นจะพาทุกๆคนเข้าไปยังห้างที่มีชื่อว่า Coroutine แต่ห้างที่ว่านี้ไม่มีที่จอดนะ ต้องไปหาจอดข้างกันเอาเองตามข้างทาง 20 30 บาท ว่ากันไป เมื่อขับไปถึงสิ่งที่ทุกคนจะเจอก็คือ พนักงานรับและจอดรถ ที่อยู่ทางเข้ากันใช่ไหมล่ะ อ้าวแล้วเกี่ยวอะไรกันใจเย็นเดี๋ยวเราจะมาเราให้ฟังกัน

Free image license from www.pexels.com

ซึ่งต่อไปนี้เราจะเรียกเค้าคนนี้ว่า Dispatcher ซึ่งพนักงานคนนี้หน้าที่ก็คนพาทุกท่านไปยังที่จอดที่ยังคงว่างอยู่

Dispatcher พนักงานรับและจอดรถ 20 บาท

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

dispatcher คนนี้เค้าไม่ได้ทำงานประจำที่ลานจอดรถยนต์ อย่างเดียวนะเออไปอยู่ที่ลานจอดมอไซต์ ท่าเรือด้วยยย ซึ่งในแต่ละที่ก็มีช่องจอดแตกต่างกันไปใช่ไหมละช่องจอดที่ว่านั่นก็คือ Thread pool (Worker Thread) นั่นเองง

Thread pool : คนสำหรับคอยจัดการการทำงานแบบ พร้อมๆ กันในเวลาเดียวกัน (Concurrentcy Programing) และจัดสรรทรัพยากรให้เหมาะกับงาน (Thread) นั้นๆ

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

Coroutine Dispatcher interface

Dispatchers.Default : เหมาะสำหรับต้องการทำงานอะไรบางอย่างที่ใช้งาน CPU อย่างหนักหน่วง หรือซับซ้อน เช่นการจัดเรียงลำดับข้อมูล คำนวณต่างๆ โดยจะมีการ Shread Pool ร่วมกันกับ Tread อื่นๆ ด้วย

Dispatchers.IO : เหมาะสำหรับต้องการติดต่อด้านเครือข่ายต่างเช่นการดึงข้อมูลจากฐานข้อมูล, การดึง API จาก Server, การอ่านเขียนไฟล์ต่างๆ

Dispatchers.Main : เหมาะสำหรับต้องการจะอับเดทหน้าจอต่างๆ (UI) เช่น ปรับปรุงเปลี่ยนแปลง Views ต่างๆ เช่นRecyclerView เป็นต้น

Dispatchers.Unconfined : เป็นการทำงานแบบไม่เจาะจง Thread ที่จะใช้ในการทำงานโดยจะทำงานตาม Thread ที่กำลังถูกทำงานในขณะนั้น โดยจะทำงานตาม concept event-loop เพื่อไม่ให้เกิดเหตุการ Stack overflow

มาดูตัวอย่างการใช้งานกันเลย

ยังไม่เห็นภาพกันใช่ไหมละเรามาดูตัวอย่างการโหลดรูปกันต่อเลย

Coroutine Context และ Coroutine Scope

ในตอนก่อนหน้านี้น่าจะพอเห็นผ่านๆ ตากันมาบ้างแล้ววเอาล่ะเดี๋ยวในตอนนี้เราจะมาเล่าให้ฟังมามันคืออะไร

Coroutine Context

ในทุกๆ Coroutine จะมีตัวแทนของตัวมันเองอยู่นั่นก็คือ context นั่นเองซึ่งตัว context ที่เราพูดถึงนี้คือCoroutineContext interface ซึ่งในการที่เราจะต้องการใช้งานมันเราจะเรียกใช้งานผ่าน coroutineContext ที่เป็น Property หลายๆ คนอาจจะงงๆ อยู่

Coroutine Scope

ใช้กำหนดขอบเขตในการสร้าง Coroutines ให้ทำงาน ซึ่งในการใช้งานจริงเราจะต้องระบุด้วยว่า ต้องทำงานภายใต้ Coroutine builder อะไร ( async , launch , runBlocking ) แล้วก็อย่าลืมกำหนด coroutineContext ด้วยล่ะ

แล้วมี coroutineScope แบบไหนให้เราใช้กันบ้างล่ะ ?

  • GlobalScope : เป็นการสร้าง Coroutine Scope ในระดับ Global ซึ่งในที่แน่ผู้เขียนไม่แนะนำให้ใช้ เนื่องจาก Coroutine ซึ่งถูกออกแบบมาให้รองรับการทำงานในรูปแบบ Concurrency ถึงแม้ว่าจะง่ายต่อการเขียนโค้ดในทำงานในรูปแบบ Backgroud

ตัวอย่างการใช้งาน

  • MainScope : เป็นการสร้าง Coroutine Scope สำหรับ UI Component ต่างๆ ซึ่งกระบวนการสร้าง Scope ที่ว่านี้จะประกอบไปด้วย SupervisorJob และ Dispatchers.Main ซึ่งเป็น context หากต้องการนำ MainScope ไปทำงานร่วมกันกับอีก Coroutine อื่นๆสามารถใช้คำสั่ง CoroutineScope.plus (+) operator: val scope = MainScope() + CoroutineName("MyActivity")

ตัวอย่างการใช้งาน

  • runBlocking : เป็นเมธอดสำหรับบล็อคการทำงานชั่วขณะของ Thread ที่กำลังถูกทำงานอยู่ในขณะนั้นเพื่อให้ Coroutine ที่ทำงานอยู่ภายในนั้นถูกทำงานก่อนจากนั้นหลังจากจบการทำงาน Thread นั้นๆ จึงจะถูกทำงานอีกครั้ง

ตัวอย่างการใช้งาน

สำหรับใครที่อยากรู้ในรายละเอียดลึกๆ ตามไปอ่านต่อกันได้ที่นี่เล้ย

image license from https://medium.com/@elizarov

หลายคนอาจจะสงสัยว่าเอ… มันคือภาพอะไรกันนะเอาละเราจะมาแนะนำให้รู้จักกันแต่หลายๆ คนก็อาจจะพอเข้าใจนิดหน่อยกันบ้างแล้วเห็นคำสั่ง CoroutineScope.plus (+) ที่เราได้แนะนำกันไปเมื่อสักครู่ เอาล่ะเพื่อไม่เป็นการเสียเวลาอยากให้ทุกคนได้รู้จักกับ Job และ Parentjob

Job และ Parent job

Job : เปรียบเสมือนงานงานนึง หรือ task นึงที่ถูกสร้างโดยใช้คำสั่ง launch ของ Coroutine

Parent Job : เปรียบเสมือน Job หลักที่ไว้ไปสั่งงาน Job เล็กๆให้ทำงานอีกทีนึง

เช่น Job1 = ล้อหน้ารถยนต์ , Job2 = ล้อหลัง ซึ่งทำงานที่แตกต่างกันไป
ParentJob เปรียบเสมือนพวงมาลัย และ คันเร่งที่คอยสั่งงาน Job1 , Job2 ให้ทำงานตามต้องการ

Free image license from www.pexels.com

หลังจากที่เราได้เรียนรู้กันมาพอสมควรแล้วเอาล่ะ เดี๋ยวเราจะยกตัวอย่างการใช้งานจริงให้ทุกคนได้ดูกันว่ามีรูปแบบการใช้งานในรูปแบบไหนกันบ้างเริ่มต้นด้วยแบบแรกกันก่อนเลย

launch and async

ตัวอย่างการใช้งาน

จะเห็นว่า parent coroutine ในที่นี้นั่นก็คือ launch ฟังชันทำงานภายใต้MainDispatcher

Coroutine ถูกที่เป็นตัวลูก (Child) ถูกสร้างขึ้นมาโดยผ่านฟังก์ชันasync ซึ่งทำงานภายใต้เทรธ IO IoDispatcher

การสร้างในรูปแบบนี้ coroutine จะทำการรอผลลัพธ์ที่ได้จากการทำงานของ child coroutine เสมอ

หากไม่มีผลลัพธ์ที่ได้จากการทำงานส่งกลับออกมา อาจส่งผลทำให้ application เกิด crash ขึ้นได้ ซึ่งสามารถป้องกันได้ด้วยการนำไป wrapping async ด้วย coroutineScope

ตัวอย่างการใช้งาน

launch and withContext

จากก่อนหน้านี้ที่เราได้แนะนำกันไปทุกอย่างเหมือนจะทำงานได้ดี แต่จะดีกว่าให้ถ้าเราทำให้ coroutine ที่ทำงานในครั้งถัดไปยังคงทำงานบน backgroud เราสามารถปรับปรุงโค้ดของเราได้อีกเล็กน้อยโดยการใช้คำสั่ง withContext ในการสลับ coroutine context ของเรา

Background job จะถูกทำงานโดยการใช้ฟังก์ชัน withContext จากนั้นโค้ดเราจะถูกสลับการทำงานให้ทำงานภายใต้ IO Thread ทันทีง่ายใช่ไหมล่ะมาดูตัวอย่างกันเลย

ตัวอย่างการใช้งาน

launch and withContext (ทำงานพร้อมกัน 2 งาน ตามลำดับ : Sequentially)

มาดูตัวอย่างการใช้งานกันเล้ย

แล้วถ้าอยากจะให้มันรันพร้อมกันไปเลยละทำไงดี….

launch , async and async (ทำงานพร้อมกัน 2 งาน พร้อมๆ กัน : Parallel)

ตัวอย่างการใช้งาน

How to cancel a coroutine

หลังจากที่เราสร้าง Coroutine ขึ้นมาหลาย task มากมายก่ายกอง ถ้าเราอยากจะยกเลิกการทำงานมันล่ะจะทำยังไงได้บ้าง มาดูวิธีกันเลย

ตัวอย่างการใช้งาน

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

Boilerplate : การเขียนโค้ดการทำงานบางอย่างที่มีความซ้ำซ้อน

ลด Boilerplate ด้วย ScopesViewModels

Before จะเห็นว่าเมื่อมี Model ใหม่ๆถูกประกาศเข้ามาเราจะต้องคอยประกาศคำสั่งซ้ำๆ กันก๊อปตามๆ กันมาจะดีกว่าไหมถ้าพวกนี้หายไป

After … ใช่แล้วสั้นๆ แค่นี้เลยไม่ต้องไปประกาศใหม่ซ้ำซ้อนให้เสียเวลาจะใช้งานเพียงแค่ extend ScopedViewModel เข้ามาก็พอ

….. แต่ๆ เดี๋ยวก่อนเชื่อว่ามีบางคนแอบก้อป , ScopedViewModel() ไปแปะโค้ดตัวเองลองใส่แล้วใช่ไหมล่ะ…ใช้ไม่ได้หรอก… ฮ่าๆ คลาสนี้เราจำเป็นต้อง implement มันขึ้นมาก่อนถึงจะใช้งานได้ เอาละมาดูวิธีกันเลย

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

และในบทความตอนต่อไปเราจะมาแนะนำให้ทุกคนได้รู้จักกับ Kotlin Flow และนำมาใช้งานร่วมกับ Coroutine ว่าแต่มันคืออะไรนั้นอดใจรออีกสักนิดนะ

--

--