Structure ในการทำ deeplink ให้ scale ได้จากงาน Android Bangkok Conference 2020

Event Mar 26, 2021

จดบันทึกจาก 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 คือการสร้าง url มาเปิดในแอพ สามารถรองรับ url และชี้ไปในแอพของเราได้ และมี 4 component คือ scheme, host, path และ query ซึ่งก็จะเป็น key และ value เนอะ

จริงๆทางเราเคยเขียนบล็อกเกี่ยวกับ deeplink เบื้องต้นไว้แล้ว สามารถไปอ่านได้ที่นี่
การเปิด deep link เข้าหน้า android app โดยย่อ และทดสอบอย่างง่ายๆ
สวัสดีทุกท่าน วันนี้มาแบบง่ายๆ เบาๆ ถึงอาหารคลีนมันจะไม่แซ่บ แต่มีประโยชน์นะเออ กับการทำ deep link นั่นเอง (draft…
https://mikkipastel.com/deep-link-android-app/
  • เอาไป PR ให้คนรู้จักแอพเรามากขึ้น ช่วยเพิ่ม retention และ engagement
  • ใช้ในการ navigation ภายในแอพ
  • ช่วยให้แอพอื่นๆ สามารถติดต่อสื่อสารกับแอพของเราได้ สามารถ provide ข้ามแอพได้ผ่าน deeplink เช่น แอพ LINE MAN สามารถ provide deeplink ผ่านแอพ Wongnai ได้

และเราจะต้องเพิ่ม intent filter ในส่วน data ใน activity ที่ AndroidManifest.xml นั่นเอง โดยประกาศ scheme และ host

และเรียกใช้งานต่อใช้ต่อใน DeepLinkActivity ผ่าน activity โดยการใช้ intent data

  • Decentralized : เป็นการผูก deeplink เข้าหน้านั้นๆโดยตรง เช่นในภาพก็จะมี 3 deeplink แยก feature กันไปเลย ข้อเสียคือ ถ้ามีเยอะๆ เช่น 40-50 deeplinks จะทำให้ AndroidManifest ของเราบวมได้
  • Centralized (popular) : นิยมใช้กัน คือ สร้าง class อันนึงมาจัดการ เช่น สร้าง DeeplinkActivity ขึ้นมา และ deeplink ทุกตัวจะวิ่งผ่านมาใน activity นี้ ซึ่งมันจะเป็นคนจัดการว่า deeplink นี้ให้ไปที่ไหนต่อ

แบบ 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 ไหนไหม ที่ช่วยแก้ปัญหาเหล่านี้ ก็เลยเจอบทความนี้

Navigation in Modular Applications with Deep Linking
As time goes by applications tend to grow. Growing from a minimum viable product (MVP) to a feature-rich product is an exciting phase in each company/startup — both from a business as well as from a…
https://blog.usejournal.com/navigation-in-modular-applications-with-deep-linking-6a599c11e487

ก็เลยหยิบบางส่วนมา 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 command
  • execute() ให้ 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
airbnb/DeepLinkDispatch
A simple, annotation-based library for making deep link handling better on Android - airbnb/DeepLinkDispatch
https://github.com/airbnb/DeepLinkDispatch
DeepLinkDispatch: a simple, annotation-based library for making deep link handling better on…
Deep links provide a way to link to specific content on either a website or an application. These links are indexable and searchable, and can provide users direct access to much more relevant…
https://medium.com/airbnb-engineering/deeplinkdispatch-778bc2fd54b7
  • Jetpack Navigation by Android
Navigation | Android Developers
Use the Navigation component in Android Jetpack to implement navigation in your app.
https://developer.android.com/guide/navigation

สรุป :

  • เลือก structure ที่เหมาะสมกับโปรเจกของเรา
  • แบ่ง module deeplink แล้วให้ module อื่นเห็นได้ และ module app เป็นคน implement

download แอพอ่านบล็อกใหม่ของเราได้ที่นี่

MikkiPastel - Apps on Google Play
First application from “MikkiPastel” on play store beta feature- read blog from https://www.mikkipastel.com by this application- read blog content by chrome custom tab- update or refresh new content by pull to refresh- share content to social network
https://play.google.com/store/apps/details?id=com.mikkipastel.blog

ติดตามข่าวสารและบทความใหม่ๆได้ที่

อย่าลืมกด like กด share บทความกันด้วยนะคะ :)

Posted by MikkiPastel on Sunday, 10 December 2017

ช่องทางใหม่ใน Twiter จ้า

Tags

Minseo Chayabanjonglerd

I am a full-time Android Developer and part-time contributor with developer community and web3 world, who believe people have hard skills and soft skills to up-skill to da moon.