Structure ในการทำ deeplink ให้ scale ได้จากงาน Android Bangkok Conference 2020
จดบันทึกจาก session ที่ชื่อว่า "Don't destroy your app structure with bad Deep Link implementation" โดยคุณ Nakharin Sanguansom เป็น Senior Software Engineer (Android) จาก LINE MAN Wongnai จ้า
เนื่องจากเรานั้นตั้งใจไม่ปล่อยบล็อกสรุปงาน Android Bangkok Conference 2020 เอง เพราะอยากลองสรุป session ที่เราสนใจ ออกมาเป็นบล็อกเดี่ยวๆ พร้อมการตกผลึกบางอย่าง เช่น นำไปคุยกับทีม หรือลองเอาไปทำเอง ทำให้บทความไม่ยาวมากจนเกินไปอะเนอะ
แต่เราก็ดองมาหลายเดือนหล่ะ และมีการเลือก session ไปทำสรุปบ้างแล้ว แต่เพิ่งเริ่มทำเองจ้า เพราะอย่างอื่นชั้นก็ดองไว้เหมียนกัน ถ้าเป็นไข่แดงดองน่าจะอร่อยน่าดูเนอะ >w<
สาเหตุที่เราหยิบ session นี้มาสรุป เพราะเกิดจากการคุยกันในทีมแหละ เกี่ยวกับ deeplink สำหรับโปรเจกใหม่ที่เราทำ แล้วมีหัวหน้ากับเราสองคนที่เข้าไปฟัง session นี้มาก่อน เลยเข้าใจกันแค่สองคน คนอื่นๆในทีมงงๆ ซึ่งทีมเราก็ควรศึกษาและนำไปประยุกต์ใช้เนอะ
ใน session นี้กล่าวถึง structure ที่ดีในการทำ deeplink ให้ scale ได้นั่นเอง
โดย case study จริงก็คือมาจากการทำแอพ LINE MAN บน Android นั่นแหละ และแอพนี้มีมากถึง 50 deeplinks ด้วยกัน! และต้องการให้ deeplink สามารถ scaleable ไปได้ และ easy to maintain
Deeplink คืออะไรหยอ?
deeplink คือการสร้าง url มาเปิดในแอพ สามารถรองรับ url และชี้ไปในแอพของเราได้ และมี 4 component คือ scheme, host, path และ query ซึ่งก็จะเป็น key และ value เนอะ
จริงๆทางเราเคยเขียนบล็อกเกี่ยวกับ deeplink เบื้องต้นไว้แล้ว สามารถไปอ่านได้ที่นี่
ทำไมแอพเราถึงต้องมี deeplink หล่ะ?
- เอาไป PR ให้คนรู้จักแอพเรามากขึ้น ช่วยเพิ่ม retention และ engagement
- ใช้ในการ navigation ภายในแอพ
- ช่วยให้แอพอื่นๆ สามารถติดต่อสื่อสารกับแอพของเราได้ สามารถ provide ข้ามแอพได้ผ่าน deeplink เช่น แอพ LINE MAN สามารถ provide deeplink ผ่านแอพ Wongnai ได้
และเราจะต้องเพิ่ม intent filter ในส่วน data ใน activity ที่ AndroidManifest.xml
นั่นเอง โดยประกาศ scheme และ host
และเรียกใช้งานต่อใช้ต่อใน DeepLinkActivity
ผ่าน activity โดยการใช้ intent data
Solution ในการพัฒนา Deeplink
- Decentralized : เป็นการผูก deeplink เข้าหน้านั้นๆโดยตรง เช่นในภาพก็จะมี 3 deeplink แยก feature กันไปเลย ข้อเสียคือ ถ้ามีเยอะๆ เช่น 40-50 deeplinks จะทำให้
AndroidManifest
ของเราบวมได้
- Centralized (popular) : นิยมใช้กัน คือ สร้าง class อันนึงมาจัดการ เช่น สร้าง
DeeplinkActivity
ขึ้นมา และ deeplink ทุกตัวจะวิ่งผ่านมาใน activity นี้ ซึ่งมันจะเป็นคนจัดการว่า deeplink นี้ให้ไปที่ไหนต่อ
ปัญหาที่พบเจอในท่า popular
แบบ Centralized ก็จะมีปัญหาอยู่เหมือนกัน เพราะเป็นการกระจุกตัวของโค้ดไปอยู่ใน class เดียว แล้ววันนึงก็จะแปลงร่างไปเป็น GOD Class ได้ ทำให้เกิดปัญหาอื่นๆตามมาได้
- Structure problem ตัวอย่างโค้ดที่เขายกมา ก็น่าจะมี 3 ประเภท คือ
1) เรียกไป activity นั้นเลย
2) ดึงค่า query จาก deeplink แล้วโยนค่าไป activity ถัดไป
3) เรียก api เพื่อเอาอะไรบางอย่างนำไปใช้งานใน activity นั้นๆ ซึ่งอันนี้ก็เริ่มซับซ้อนหล่ะ
และถ้ามีเยอะๆคือบวมแน่นอน
- Back Stack Problem : เปิดผ่าน deeplink ซึ่งไม่ได้เปิดผ่านหน้าอื่นๆ แล้วถ้ากด back แล้วจะเจออะไรต่อ ก็คือออกแอพไปเลย (ตามประสบการณ์ของเราคือโดน tester แจ้งการ์ดบัคเลย ;__;) ดังนั้นต้องทำให้ user อยู่กับแอพเราเหมือนเดิม โดยการกด back หลังจากกดเข้ามาจาก deeplink แล้ว จะต้องเหมือนตอนที่ user กดเข้าหน้านั้นมาจริงๆ
- Hard to Maintain : ดูยากและตาลายเพราะมีหลายบรรทัดเกินไป
- No Local Unit Test : เขียน test ที่ activity ยาก ต้องเขียน UI Test ซึ่งมันก็ยากไปอีก
มี Solution ที่ดีกว่าไหมมมมม?
ก่อนจะมารู้จัก solution ที่ดีนั้น ต้องรู้ background ของเหตุการณ์ก่อนว่า แอพ LINE MAN นั้น
- มีทั้งหมด 50 กว่า deeplinks
- มี GOD Class ที่เล่าไป
- ใช้ effort ค่อนข้างเยอะในการที่เราจะเพิ่ม deeplink ใหม่สักตัว ต้อง make sure ว่าไม่กระทบ deeplink อันอื่นน้าาา
- scaleable ไม่ได้
- testing คือไม่ต้องพู๊ดดดดด (//เสียงพี่เหยก) เพราะมันไม่มี
ทีม Android เลยคุยกันว่ามี structure ไหนไหม ที่ช่วยแก้ปัญหาเหล่านี้ ก็เลยเจอบทความนี้
ก็เลยหยิบบางส่วนมา adapt และสร้างเป็น patterns ในแอพ LINE MAN จนออกมาเป็น conponents 3 ตัว
- Launcher : แหล่งรวมของ processor และทำ tacking & logging ที่ layer นี้
- Processor : ตัวคนที่ตัดสินใจว่าให้ deeplink หรือ command ตัวไหนทำงาน
- Command : มีหน้าที่ทำงาน deeplink ตามที่เราโปรแกรมไว้เฉยๆ และจัดการเคส handle stack เคสต่างๆให้ด้วย
และรูปนี้ก็คือ design pattern ที่ชื่อว่า command pattern กับ deeplink ของเรานั่นเอง การทำงานก็คือ เมื่อมี url เข้ามาจะผ่านที่ Launcher ไปยัง Processor จากนั้น Processor จะเลือกว่าให้ไป Command ไหน และ Command ก็จะพาไปที่ Activity นั้น
ในส่วนของ Command เราจะสร้าง class ที่ชื่อว่า DeepLinkCommand
เป็น Interface Class ขึ้นมา และมี 4 methods ที่สำคัญ คือ
matches()
เอาไว้ match commandexecute()
ให้ deeplink ทำงานอะไร และมีisTaskRoot
check ว่า ถ้าเป็น true ให้ทำonHandleNoStack()
และถ้าเป็น false ให้ทำonHandleHasStack()
onHandleNoStack()
เมื่อไม่มี stack ให้ทำอะไรต่อonHandleHasStack()
เมื่อมี stack ให้ทำไรต่อ
และ isTaskRoot
คืออะไร? ตามนิยามคือเป็น 1 ใน method ที่อยู่ใน activity class ของ Android อยู่แล้ว สามารถบอกได้ว่า activity นั้นเป็น root ของ stack หรือไม่ (false
) หรือแอพเพิ่งถูกเปิดเป็นครั้งแรกหรือไม่ (true
)
การนำไปใช้งาน ก็สร้าง class ใหม่ที่ extend ตัว DeepLinkCommand
นั่นแหละ
เราจะทำการ match ว่า มี deeplink ที่ตรงกันไหม ถ้าตรงก็ไป execute ต่อว่าไปที่หน้า FoodActivity
มี stack ไหม ถ้าไม่มี stack ให้ไปหน้า HomeActivity
ในเคสนี้ยังไม่มีการจัดการว่าถ้ามี stack ให้ทำอะไรต่อ
และถ้ามี stack หล่ะ เราจะทำยังไงต่อดี?
ทำการประกาศ global variable ขึ้นมา ชื่อว่า flags
ก็คือ intent flag ที่แนบเข้าไปกับ intent และเอามาใส่ที่ onHandleHasStack()
ในที่นี้คือ clear stack ทั้งหมดนั่นเอง
มาถึง Processor กันบ้าง มี function เดียวคือ process()
โดยตัว FoodDeepLinkProcessor
จะรับ command ที่เป็น set หรือ list เข้ามา และ processor จะเป็นคนตัดสินใจว่าจะให้ deeplink ไหนทำงาน โดยเอามาวน forEach ถ้าตรงกันก็ให้ทำ execute
ด้วย
ถ้าอยากให้ทำงานเป็น parallel เปลี่ยนจาก return ออกไปเลยเป็นมา return ข้างนอกแทน เป็นการ check ว่ามี deeplink ที่ตรงกันหม๊ายย
ต่อมาที่ Launcher เป็นแหล่งรวมของ depo อย่างแอพ LINE MAN ก็มีหลายๆ service ด้วยกัน ก็จะแยกเป็น service ต่างๆ ไม่ว่าจะเป็น food เป็น taxi และมี legacy อยู่ด้วย เพราะว่าการพัฒนาและ migrate นั้น ไม่สามารถทำให้จบภายใน sprint เดียวได้ จึงต้องทำ callback กลับไป ถ้าไม่ match กับ pattern ใหม่ของเรา
และนอกสุด DeepLinkActivity
เอา DeepLinkLauncher
ไปใส่ไว้ และมีการส่งใน intent data แล้วส่ง uri ส่งไปให้ launcher ทำงาน
มีการ check isTaskRoot
ตรงนี้ด้วย ถ้า DeepLinkActivity
ถูกเปิดขึ้นมาเป็นครั้งแรกหรือไม่ เพื่อให้ command รู้ว่ามันถูกเปิดขึ้นมาเป็นครั้งแรกหรือเปล่า
แล้วก็มีตัวอย่างให้ดูใน session ด้วยนะ
ปัญหาสามัญ
- User ยังไม่ได้ login → ให้ user ไป login ก่อน แล้วค่อย process deeplink ตัวนั้นว่าจะไปที่หน้าไหนต่อ จากโค้ดตัวอย่างก็คือ ถ้ายังไม่ได้ login ก็จะให้ไป save deeplink ไว้ใน cache แล้วให้ทำงานภายหลัง หลังจากที่ user login แล้ว
- deeplink ที่ต้องการข้อมูลจาก API ก่อน → ใน command สามารถ inject UseCase เข้ามาได้ และเรียก api ตรง
execute()
เมื่อได้ผลลัพธ์ก็ทำงานตามแต่ที่เราต้องการได้เลยจ้า และจริงๆ ใช้ coroutine ในการจัดการแต่พื้นที่สไลด์ไม่พอเลยเป็น callback ไปก่อน เพื่อให้เข้าใจได้ง่ายเนอะว่าเอาไปทำไรต่อ
- deeplink ที่เป็น popup dialog ที่สามารถโชว์ได้ทุกหน้าด้วย ในแอพของเรา → เช่น เรียก api แล้วไม่ผ่าน ก็จะขึ้น popup dialog ขึ้นมา ก็เลยสร้าง
CurrentActivityProvider
ขึ้นมา เป็น class ที่เอาไว้เก็บ activity ที่กำลัง run อยู่
แล้วเราจะเก็บ activity ที่กำลัง run อยู่ได้อย่างไรหล่ะ? เก็บได้โดยเรียก registerActivityLifecycleCallbacks
ที่ application class ของเรา บอกได้ว่า activity อะไรที่เรา run อยู่ ในที่นี้จะใช้ currentActivityProvider
เก็บตรง onActivityStarted()
หลังจากเปลี่ยน strucuture ของ deeplink พบว่าทีมมีคุณภาพชีวิตที่ดีขึ้น ทั้ง structure ที่ดี, ง่ายต่อการ maintain, สามารถ scale ได้ และ สามารถ test ได้
การเขียนเทส
ในส่วน processor จะเพิ่ม matchedCommands
เป็น list ตัวนึง
ส่วน command เพิ่ม tag()
และในทุกๆตัวที่ extend มาจาก DeepLinkCommand
นั้น ให้มัน provide tag()
ให้ด้วย ก็คือชื่อ class ของตัวมันเองนั่นแหละ
และใน processor ก็ implement ตามนี้เลย ถ้า command ไหนที่มัน match ให้เก็บ tag ไว้ลงใน matchedCommands
ของเราด้วยนะ
เท่านี้เราก็จะทำ Unit Test ได้แล้ว เย้ๆ
เช่นเราจะทำการ test ที่ FoodDeepLinkCommand
ก็จะทำการ provide deeplink เข้ามา แล้วส่ง deeplink เข้าไปให้ processor เพื่อให้ processor ทำงานตามต้องการ และ test ว่า matchedCommands.size
เท่ากับ 1 หรือเปล่า และตัวที่เรา match ตรงกับที่เราต้องการหรือเปล่า
ทางเลือกอื่นๆ
- DeepLinkDispatch by Airbnb
- Jetpack Navigation by Android
สรุป :
- เลือก structure ที่เหมาะสมกับโปรเจกของเรา
- แบ่ง module deeplink แล้วให้ module อื่นเห็นได้ และ module app เป็นคน implement
download แอพอ่านบล็อกใหม่ของเราได้ที่นี่
ติดตามข่าวสารและบทความใหม่ๆได้ที่
ช่องทางใหม่ใน Twiter จ้า