[Android] มาทำความรู้จักกับโปรเจค Android ของเราให้มากขึ้นดีกว่า (part 1)

Kajornsak P.
6 min readMar 22, 2021

โดยปกติแล้ว เมื่อโปรเจคของเรามีขนาดใหญ่ขึ้น การที่เราจะวางแผนในการวางโครงโปรเจคก็จะเป็นเรื่องที่ยากขึ้น ยิ่งถ้าเราไม่รู้ว่าโปรเจคเรามีปัญหาอะไรอยู่รึเปล่า ก็จะทำให้เราไม่เข้าใจในโปรเจคมากขึ้นไปอีก

การวางโครงโปรเจคที่ดี ก็เหมือนการจัดสายไฟให้ตู้ server น่ะแหละ! : Photo by Science in HD on Unsplash

ซึ่ง! ปัญหาหลักๆที่เราจะเจอกันก็คือ การที่โปรเจคเรา build ช้านั่นเอง ซึ่งอาจจะมาจากสาเหตุหลายๆอย่างด้วยกัน ไม่ว่าจะเป็นทั้งด้าน Hardware ที่อาจจะเก่า หรือมีสเปคที่ไม่ได้สูงมาก หรือ Software…ก็คือโค้ดเรานั่นแหละ ที่ส่งผลต่อการ build

ซึ่งอาจจะมาจากการที่โปรเจคของเราไม่ได้มีการ Modularized ที่ดีพอ หรือเราอาจจะ Modularized เต็มที่แล้ว แต่มีการใช้งาน dependencies ที่พันกันยุ่งเหยิง ก็อาจส่งผลให้โปรเจคเรา build ช้าได้เช่นกัน

เอ๊ะ แล้วเราจะรู้ได้ยังไงว่าโปรเจคเรา build ช้ารึเปล่า มีปัญหาอะไรรึเปล่านะ

ก่อนที่เราจะรู้ว่าโปรเจคเรา build เป็นยังไง ช้าหรือเร็ว แปลว่าเราต้องมีการเก็บข้อมูลกันก่อน ก่อนที่เราจะมาวิเคราะห์กันว่าปัญหามาจากอะไร ซึ่งมีมากมายหลายวิธี หนึ่งในวิธียอดนิยมก็คือการใช้ Gradle Build Scan ใน Gradle Enterprise นั่นเอง

แต่! ถ้าเราทำงานคนเดียว หรือทีมเรามีขนาดไม่ใหญ่ เราอาจจะไม่ถึงกับต้องเสียเงินใช้ Gradle Enterprise ก็ได้ วันนี้เราจะมาลองวิธีอื่นกัน!

คำเตือน บทความนี้จะพูดถึง Build system ที่เป็น Gradle อย่างเดียว ถ้าใครใช้ตัวอื่นๆเช่น Buck หรือ Bazel ก็…ยินดีด้วยครับ คุณได้ยกระดับไปอีกขั้นแล้ว 😂

การวิเคราะห์ dependencies ของโปรเจค

แอบไม่แน่ใจว่ามันจะช่วยให้เข้าใจมากขึ้นจริงๆใช่มั้ย 😅

การที่เราจะเข้าใจโครงสร้างของโปรเจคเรา เราต้องทำความรู้จักกับสิ่งๆนึงก่อน เมื่อเราเข้าใจมันแล้ว เราก็จะรู้ถึงปัญหาเบื้องหลังของโปรเจคเอง

สิ่งนั้นก็คือ Dependency Graph นั่นเองง

By Laurensmast — Own work, Public Domain, https://commons.wikimedia.org/w/index.php?curid=9091762

โดยปกติแล้ว Dependency graph มักจะถูกใช้ในการอธิบายถึงโครงสร้างของสิ่งต่างๆ ที่พึ่งพากัน เช่น A rely on B ก็คือการที่ A นั้นจำเป็นต้องมี B เพื่อที่จะทำงานใดๆได้

ซึ่งใน Build system ส่วนใหญ่ การที่ โค้ดชุด A จะสามารถ Compile ได้หรือใช้งานได้ บางทีก็ต้องอาศัยโค้ด B ที่อาจจะอยู่อีก File/Module/Project ซึ่งอาจจะต้องใช้ C, D ต่อ ก็ว่ากันไปตามระเบียบ

ในโลกของ Gradle ที่เป็น Build system พื้นฐานของ Android ก็มีการใช้งาน Dependency graph เพื่อเอาไว้บอกว่าแต่ละ Module/Project นั้นพึ่งพากันอย่างไรเช่นกัน (หรือแม้กระทั่งระบบ Gradle tasks เองก็สามารถมองเป็น graph ได้)

มาถึงตรงนี้แล้ว อาจจะงงนิดหน่อย ลองมาดูตัวอย่างที่เห็นภาพดีกว่า!

สมมติว่าผมสร้างโปรเจคขึ้นมาใหม่เลยหนึ่งอัน โปรเจคก็จะประกอบไปด้วยโมดูล app ตามปกติ ก็จะไม่มี dependency ใดๆทั้งนั้น

โปรเจคเปล่าๆ แบบเพิ่งสร้างสดๆร้อนๆ

จากนั้นถ้าเราลองกด Build แอปออกมา ก็จะเห็นว่า Gradle จะ build เฉพาะ Module app เท่านั้น (เพราะมีอยู่แค่ module เดียว)

ถ้าดูจาก Build Analyzer ก็จะเห็นว่า build แค่ module app เท่านั้น

ในเคสนี้ dependency graph ของเราก็จะไม่มีอะไรเลยเช่นกัน

จบ ปิ๊ง~

แต่!!! ในโลกของความเป็นจริงและการทำ Modularization นั้นไม่ได้สวยงามเหมือนกับโปรเจคที่เพิ่งสร้างขึ้นมาใหม่ๆ ตามหลักแล้วเราจะมีการแยกโค้ดออกเป็นหลายๆ Module เพื่อแบ่งหน้าที่กันให้ชัดเจน และเพื่อ​ Scalability ของโปรเจคด้วยเช่นกัน

ดังนั้น เราจะมาลองดูตัวอย่างที่ซับซ้อนขึ้นซักหน่อย

คราวนี้ผมจะเสก Android Library Module ขึ้นมาอีกสองอัน เรียกว่า Module aa และ Module bb (อย่าไปตั้งชื่อแบบนี้จริงๆนะ เดี๋ยวเพื่อนร่วมงานจะมองแรงใส่) โดยที่เราจะยังไม่เรียกใช้อะไรในสอง Module นี้เลย จากนั้นก็ลอง build ดูหนึ่งที

ลองดูที่ Build Analyzer คู่ใจ

จะเห็นได้ใน Build Analyzer ว่าไม่มีอะไรเกี่ยวกับ aa, bb ทั้งสิ้น นั่นเป็นเพราะเราไม่ได้เรียกใช้อะไรใน 2 module นี้เลย ซึ่ง Gradle เองก็รู้เช่นกัน (ทุกคนรู้ Gradle รู้) มันเลยไม่ทำการ compile โค้ดให้เสียเปล่า ซึ่งตอนนี้ Dependency graph ของเราก็จะหน้าตาประมาณนี้

สวยงาม ถูกต้องตามโปรเจคเป๊ะๆ

ถ้าอย่างงั้น แปลว่า จำนวน Module ที่มากเกินไป ไม่มีผลกับการ build ช้า ตราบใดที่เราไม่เรียกใช้มัน (หรือเรียกใช้มันอย่างถูกต้องตามหลัก Dependency chain) อาจจะแค่ทำให้ Android Studio indexing ช้าเฉยๆ แต่ไม่กระทบการ build แน่นอน!

งั้นเรามาลองดูอะไรที่ซับซ้อนขึ้นมาอีกกันดีกว่า

คราวนี้ ผมจะทำให้ app มีการเรียกใช้งาน Module aa และ bb โดยการเพิ่ม Dependency เข้าไปใน build.gradle ของ app จากนั้นก็ลองทำการ build ดู

จะเห็นได้ว่า กว่าที่เราจะได้ app ออกมา (จาก assembleDebug) ตัว Gradle จะทำการ build ทั้ง aa และ bb ด้วยเช่นกัน เนื่องจากตอนนี้ app เรา depends on ทั้ง aa และ bb ถ้าดู Dependency graph ก็จะออกมาประมาณนี้

จะเริ่มเห็นได้ว่า การที่มี dependency นั้นส่งผลต่อระยะเวลาในการ build อย่างแน่นอน โดยเฉพาะถ้ามีการ depends ซ้อนกันหลายๆชั้น ก็จะทำให้ Ddependency graph ของเรานั้นยาวขึ้นด้วย

ยกตัวอย่างเช่น Module aa นั้น depends on Module bb อีกทีนึง ก็จะเป็นแบบนี้ ซึ่งใน gradle เราสามารถเลือกได้ว่า จะใช้ api หรือ implementation เดี๋ยวเราจะมาพูดถึงเรื่องนี้กันอีกที

ซ้าย: api, ขวา: implementation จะเห็นว่าสีเส้นไม่เหมือนกันน

หรือถ้ามีจำนวน Module เยอะๆแล้วมีการใช้ต่อๆกัน ก็จะเป็นแบบนี้

และถ้ายิ่งโปรเจคของเราใหญ่ขึ้นตามสเกลของแอปเรา ก็จะเห็นภาพ dependency graph ที่ใหญ่ขึ้นอย่างเห็นได้ชัด ยกตัวอย่างเช่นแอป iosched ก็จะเป็นดังนี้

ซึ่งหนึ่งในข้อดีของการที่ Gradle ใช้ระบบ dependency graph ก็คือ ความสามารถในการเช็คว่าเรามี dependency ที่ชี้หากันเองมั่วรึเปล่า เช่น aa ใช้ bb และ bb ก็ใช้ aa ในเคสแบบนี้เราจะเรียกว่า Directed cycle graph ซึ่งตามหลักแล้วถ้ามีการ build เกิดขึ้น มันจะเป็น Infinited loop เหมือนงูกินหาง Gradle เลยสามารถช่วยเราเช็คไม่ให้เกิดเหตุการณ์แบบนี้ได้นั่นเอง

เราจะไม่สามารถ build ได้เลยถ้ามี circular graph เกิดขึ้น

เพราะฉะนั้น การที่เราจัดระเบียบโครงสร้างโปรเจคของเราให้เป็นสัดส่วน มีการแบ่งแยกหน้าที่กันชัดเจน และวาง Dependency chain ให้ถูกต้อง จะช่วยให้เราแก้ปัญหาการ build ช้าได้ง่ายขึ้นมากๆ

โดยเฉพาะในเคสที่เราเปิด parallel build ให้กับโปรเจคของเรา ฝั่ง Gradle เองก็จะพยายาม build ของที่ไม่เกี่ยวข้องกันให้มากที่สุดเท่าที่จะทำไหว ก็จะช่วยลดระยะเวลาในการรอ build แต่ละ module ไปด้วย ตามตัวอย่างในรูปข้างล่างเลยย

จะเห็นว่า module ที่เป็น leaf node จะไม่ได้พึ่งพาใคร เลย build ได้เลยแบบไม่ต้องรอ

ในส่วนของเรื่อง api vs implementation เอง ก็เป็นสิ่งที่มีผลต่อ build time เช่นกัน เนื่องจากตัว java library plugin นั้นจะถูก compile แบบไหน ขึ้นอยู่กับว่าเราใช้ Configuration แบบไหนสำหรับตัว dependency

แล้ว api กับ implementation ต่างกันอย่างไร

ในสมัยก่อนนั้น (ตั้งแต่ Gradle ก่อนเวอร์ชั่น 3.4) ยังไม่มีสิ่งที่เรียกว่า Java library plugin ซึ่งเราจะใช้ keyword compile ในการ compile ทั้งหมด โดย dependency ทั้งหลายจะไม่มีการแบ่งแยก classpath ออกจากกัน ทำให้มีโอกาสที่ Module/Project ด้านบน สามารถมองเห็น classpath ของ dependency ข้างล่างได้ทั้งหมด

ดังนั้น เพื่อแก้ปัญหาเหล่านี้ และเพื่อ build time ที่เร็วขึ้น ใน Gradle 3.4 เลยมีการเปิดตัว Java library plugin ขึ้นมาพร้อมกับการใช้ api, implementation นั่นเอง ส่งผลให้ Android Gradle Plugin ตั้งแต่ 3.0 เป็นต้นมา ได้รับความสามารถนี้ของ Gradle มาด้วยเช่นกัน

แล้วการแบ่งที่ว่า หมายถึงแบ่งแบบไหนนะ?

ซ้าย aa implementation cc และ ขวา bb api dd

จากรูปด้านบน จะเห็นว่า Module app ของเรานั้นมี aa และ bb เป็น Dependency อยู่ ซึ่ง aa ก็มี Dependency เป็น cc และ bb ก็มี Dependency เป็น dd เช่นกัน โดยแตกต่างกันที่ aa เรียกใช้ cc แบบ implementation และ bb เรียกใช้ dd แบบ api

สิ่งที่เกิดขึ้นก็คือ เมื่อเราเรียกใช้ Dependency ด้วย implementation นั้น คนที่เรียกใช้ library นั้นอีกที จะไม่สามารถมองเห็นของที่อยู่ด้านล่างได้ ในรูปนี้ก็คือ Module app ของเรา (เรียกว่า Consumer ก็ได้) จะมองเห็นของที่อยู่ใน aa และเรียกใช้งานได้ปกติ แต่จะมองไม่เห็นของที่อยู่ใน cc เลยไม่สามารถเรียกใช้งานได้

แต่ในทางกลับกัน ถ้าเราเรียกใช้ Dependency ด้วย api แทน Consumer ของเรา (Module app) จะมองเห็นของที่อยู่ใน bb และเรียกใช้งานได้ (ปกติ) และ!! จะมองเห็นของใน dd และเรียกใช้งานได้ด้วยเช่นกัน

ซึ่งความแตกต่างของการใช้ api และ implementation นั้นมีทั้งข้อดีและข้อเสีย โดยอยู่ที่วิจารณญาณและความจำเป็นในการใช้งานเลย แนะนำให้เข้าไปอ่านเพิ่มเติมที่ User guide ใน Gradle ครับ เขียนไว้ค่อนข้างละเอียดเลยทีเดียว (วาร์ป)

ซึ่งในบทความนี้เราจะสนใจเฉพาะส่วนที่มีผลต่อ Build time เท่านั้น ซึ่งการเรียกใช้ Configuration แบบ api/implementation เนี่ย มันมีผลต่อการ recompile อย่างเห็นได้ชัด เลยเป็นที่มาว่าทำไมถึงต้องเลือกใช้ให้เหมาะสม ยกตัวอย่างเช่น

ซ้าย-Implementation, ขวา-api

เมื่อเกิดการแก้ไขโค้ดที่ระดับล่างสุด (cc หรือ dd) เจ้าตัว Gradle ก็จะเริ่มหาว่าจาก dependency graph นี้ มีใครบ้างที่ได้รับผลกระทบ และควรจะ recompile เฉพาะคนที่เกี่ยวข้องเท่านั้น

เคสแรก implementation (ซ้าย)

จะเห็นว่าด้วยความที่ Module app (Consumer) นั้นไม่รับรู้ถึงการมีอยู่ของ cc ด้วยซ้ำ 😿 ทำให้ Module app นั้นไม่มีความจำเป็นต้องถูก Compile ใหม่ให้เสียเวลา Gradle เลยจะจับเฉพาะ aa และ cc มา compile ใหม่เท่านั้น (เพราะ aa ใช้งาน cc แบบตรงๆอยู่แล้ว)

เคสที่สอง api (ขวา)

ในทางกลับกัน Module app (Consumer) นั้นรับรู้ถึงการมีอยู่ของ dd และอาจมีการเรียกใช้ด้วยเพราะเข้าถึงได้ปกติ ทำให้ Gradle นั้นจำใจต้อง compile Module app ใหมด้วยเช่นกัน ซึ่งจะกลายเป็นว่า เราแก้ไขโค้ดที่เดียว แต่ต้อง recompile ใหม่ยันระดับ Consumer เลยทีเดียว

เพราะฉะนั้น การเลือกใช้ Configuration สำหรับ Dependency ของเราก็สำคัญเช่นกันครับ แนะนำให้ลองอ่าน User guide ของ Gradle อีกทีจริงๆนะ 🥺 แปะลิงค์ให้แบบชัดๆไปเลย

มาถึงจุดนี้แล้ว หลายๆคนอาจจะสงสัยว่า เอ๊ะ ถ้าเราขึ้นโปรเจคมาซักพักแล้ว แต่ไม่เคยลองไล่ดู dependency graph เลยว่าเป็นยังไง เรามีวิธีในการไล่ดูรึเปล่านะ?

มี!!! และง่ายมากด้วยยย

จากตัวอย่างที่เห็นด้านบน ต้องบอกว่าผมไม่ได้วาด Dependency graph เองเลย เพราะถ้าวาดเองคงใช้เวลาหลายวัน 😂 ซึ่งในบทความนี้ผมใช้ตัวช่วยในการวาดแบบไม่ต้องใช้แรง เป็น Gradle task สำหรับ analyze ตัว plugin dependency ในโปรเจคเราแบบง่ายๆ ซึ่งมาจากเสด็จพ่อ JakeWharton นั่นเองง 🙇

วิธีเอามาใช้แบบง่ายๆเลยก็คือ

  1. ติดตั้ง graphviz ลงในเครื่องก่อน เลือกลงตามความชอบได้เลย ในที่นี้ผมก็แค่ลงผ่าน Homebrew ด้วย brew install graphviz
  2. เอาโค้ดส่วน projectDependencyGraph.gradle มาเพิ่มในโปรเจคของเรา จะไว้ที่ไหนก็ได้เช่นกัน อาจจะเอาไว้ใน folder gradle เพื่อความสะดวก
  3. จากนั้นก็ apply เจ้า gradle ไฟล์นี้ไว้ใน build.gradle, build.gradle.kts ของ Module/Project ที่เราต้องการดู dependency graph (อย่าลืม reference path ให้ถูกนะ!)
แล้วแต่ความสะดวกเลย โดยในเคสนี้ผมเรียกใช้ที่ build.gradle ของ project-level

จากนั้นก็ทำการ Sync Gradle ทีนึง เพื่อให้ task นี้ถูกเพิ่มเข้าไปใน Module/Project ของเรา แล้วก็ทำการเรียกใช้ task นี้ตามสะดวกเลย (ทั้งใน Gradle tab ด้านขวา หรือจะผ่าน Command line แบบเกร๋ๆก็ได้)

เมื่อรันเสร็จแล้ว ก็กดดูไฟล์ png ที่ออกมาได้เลย โดยจะอยู่ที่ build ของ root project นั่นเอง

ทีนี้เราก็จะเริ่มเข้าใจใน dependency ของโปรเจคเรามากขึ้นแล้ววว 🎉

แต่!! ถ้าเราลองจัดระเบียบโปรเจคแล้วมันยังไม่ดีขึ้นเลยล่ะ ยัง build ช้าเหมือนเดิมเลย ควรจะทำยังไงดีต่อ ซึ่งผมก็มีวิธีมานำเสนอเหมือนกัน สามารถเข้าไปติดตามได้ที่ part 2 ของบทความชุดนี้ได้เลย~

มาทำความรู้จักกับโปรเจค Android ของเราให้มากขึ้นดีกว่า (part 1) (อันนี้)

มาทำความรู้จักกับโปรเจค Android ของเราให้มากขึ้นดีกว่า (part 2)

มาทำความรู้จักกับโปรเจค Android ของเราให้มากขึ้นดีกว่า (part 3) (เร็วๆนี้)

--

--

Kajornsak P.

Android & iOS developer. Interest in UI/UX design. Currently, Senior iOS Engineer at Agoda — Mobile Platform team