ในบทความนี้ เราจะพูดถึงโปรเจกต์หลายไลบรารี (multi-library projects) ใน Android มันไม่ใช่เรื่องปกติ แต่ก็ไม่ใช่เรื่องน่าประหลาดอีก
คุณอาจเคยเจอโปรเจกต์หลายไลบรารีในงานของคุณ หรือคุณอาจกำลังมองหาการแปลงไลบรารีของคุณเป็น sub-modules เพื่อโครงสร้างและจัดระเบียบที่ดีกว่า ไม่ว่าจะเป็นกรณีไหน คุณควรทราบดีว่าสิ่งที่รออยู่ข้างหน้าคืออะไรก่อนจะดำน้ำลงไป
การเขียนไลบรารีของคุณเองใน Android น่าสนใจ คุณได้โอกาสเขียนโค้ดที่ช่วยนักพัฒนาคนอื่น (หรือแม้แต่คุณเอง)
เนื่องจากไลบรารีไม่สามารถเป็นโปรเจกต์อิสระได้ด้วยตัวเอง พวกมันจึงมักจับคู่กับแอปพลิเคชันในโปรเจกต์ นี่ทำให้การพัฒนาไลบรารีเป็นกระบวนการที่เรียบง่าย โดยคุณเพิ่มฟีเจอร์/แก้บั๊ก แล้วทดสอบโดยตรงกับแอปพลิเคชันที่คุณมีในโปรเจกต์ ดังนั้น จำลอง (ในทางท้องถิ่น/local) ว่าผู้พัฒนาจะรวมไลบรารีของคุณอย่างไร
แต่ ถ้าไลบรารีของคุณขึ้นอยู่กับไลบรารีอีกตัวที่คุณกำลังพัฒนาอยู่?
หากคุณไม่รู้ คุณควรทราบว่า ไลบรารี (อ่านว่า aar) ไม่สามารถบรรจุไลบรารีท้องถิ่น (local library) อีกตัวภายในได้ มันสามารถขึ้นอยู่กับไลบรารีระยะไกล (remote dependencies) ได้ แต่ไม่ใช่สิ่งที่อยู่ในท้องถิ่น
Android ไม่รองรับสิ่งนี้ และแม้จะมีโซลูชันบางอย่างขึ้นมาตลอดปี (FatAar) แต่พวกมันไม่ได้แก้ปัญหาเสมอไปและไม่ทันสมัย มี Google Issue Tracker ขอฟีเจอร์นี้เปิดมานานแล้วและได้รับความสนใจมากจากชุมชน แต่ลองระบุว่าผนังไหนเราสามารถทำลายได้ และไหนไม่ได้
ลองจินตนาการว่า階層ของโปรเจกต์คุณเป็นแบบนี้:
-- App
| -- OuterLib
| --- InnerLib
ดังนั้น เนื่องจาก InnerLib ไม่สามารถเป็นส่วนหนึ่งของโปรเจกต์เดิมได้ มันจะอยู่ที่ไหน? และคุณจะสามารถทำงานในท้องถิ่น (locally) ขณะพัฒนาฟีเจอร์ภายใน InnerLib ได้อย่างไร?
เราจะตอบคำถามเหล่านี้ในบทความนี้
Git Submodule
สำหรับปัญหาทางเทคนิคส่วนใหญ่ ไม่ได้มีโซลูชันเพียงอย่างเดียว ปกติแล้วมีมากกว่าหนึ่ง แตแต่ละโซลูชันมีข้อเสียของมันเอง ทุกอย่างเป็นคำถามว่าคุณสบายใจกับข้อเสียไหนมากกว่าตอนจบวัน
เพื่อตอบคำถามแรก InnerLib สามารถอยู่ที่ไหนได้บ้าง เรามีตัวเลือกหลายอย่าง:
- ทำให้ InnerLib เป็น submodule ของโปรเจกต์เดิมของเรา
- ทำให้ InnerLib เป็น remote dependency ของตัวเอง
หากคุณไม่รู้จัก submodules ใน Git เอกสารของ Git เป็นที่ดีที่จะทำความคุ้นเคยกับมัน อ้างอิงจากมัน (ย่อหน้าแรก):
บ่อยครั้งที่ขณะทำงานบนโปรเจกต์หนึ่ง คุณต้องใช้โปรเจกต์อีกตัวจากภายในมัน 👉 อาจเป็นไลบรารีที่บุคคลที่สามพัฒนา หรือที่คุณพัฒนาแยกต่างหากและใช้ในหลาย parent projects 👈 ปัญหาทั่วไปเกิดขึ้นในสถานการณ์เหล่านี้: คุณอยากสามารถจัดการสองโปรเจกต์เป็นของแยกกัน แต่ยังคงสามารถใช้หนึ่งจากภายในอีกตัวได้
ย่อหน้านี้แสดงให้เห็นว่านี่คือ use case ของเราโดยตรง การใช้ submodule มีประโยชน์ โค้ดทั้งหมดอยู่ที่เดียว จัดการง่าย และพัฒนาในท้องถิ่นง่าย
แต่ submodules มีจุดอ่อน บางอย่าง หนึ่งคือความจริงว่าคุณต้องระวังเสมอว่า submodule ของคุณชี้ไปที่ branch ไหน ลองจินตนาการสถานการณ์ที่คุณอยู่บน release branch ใน main repository และ sub-module อยู่บน feature branch หากคุณไม่สังเกต คุณจะปล่อยเวอร์ชันของโค้ดด้วยสิ่งที่ยังไม่พร้อมสำหรับ production โอ๊ะ
ลองคิดเรื่องนี้ในทีมของนักพัฒนา ความผิดพลาดจากความไม่ระมัดระวังครั้งเดียวอาจมีค่าใช้จ่ายสูง
หากตัวเลือกแรกดูมีปัญหาสำหรับคุณ การโฮสต์ไลบรารีใน repository อีกตัวคือตัวเลือกที่สอง การตั้งค่า repository ค่อนข้างง่าย แต่คุณจะทำงานในท้องถิ่นได้อย่างไร?
การทำงานในท้องถิ่น (Working Locally)
ตอนนี้เราตั้งค่าโปรเจกต์ได้อย่างเหมาะสม เราอาจมีบรรทัดคล้ายนี้ในไฟล์ build.gradle ของ OuterLib:
dependencies {
implementation 'url_to_remote_inner_lib_repository'
}
เราจะทำให้วัฏจักรการพัฒนาประสิทธิภาพและง่ายต่อการทำงานได้อย่างไร? หากเราพัฒนาฟีเจอร์บางอย่างใน InnerLib เราจะทดสอบใน OuterLib ได้อย่างไร? หรือในแอปพลิเคชันของเรา?
โซลูชันหนึ่งที่อาจคิดขึ้นคือ import InnerLib ของเราในท้องถิ่นเข้าไปในโปรเจกต์ OuterLib ขณะที่ InnerLib ถูก .gitignored ในโปรเจกต์ OuterLib คุณทำได้ง่ายๆ โดยคลิกขวาที่ชื่อโปรเจกต์ในเมนูด้านซ้ายใน Android Studio ไปที่ New → Module
วิธี import module (ขั้นตอน 1)
จากนั้นในหน้าต่างที่เปิดขึ้น คุณสามารถเลือกตัวเลือก Import ที่มุมซ้ายล่าง:
วิธี import module (ขั้นตอน 2)
ดูง่ายและเรียบง่าย แต่มีอะไรเป็นกับดัก?
ทุกครั้งที่คุณแก้ไขไฟล์ที่เป็นของ InnerLib การเปลี่ยนแปลงจะไม่สะท้อนภายใน InnerLib เนื่องจากมันถูก ignore ดังนั้น ทุกการเปลี่ยนแปลงที่คุณอยากทำ ต้องเกิดขึ้นภายใน InnerLib แล้วคุณต้อง import มันเข้า OuterLib อีกครั้งเพื่อดูการเปลี่ยนแปลง
นี่ดูไม่ถูกต้อง ต้องมีวิธีที่ดีกว่านี้
ด้วยเพียงไม่กี่บรรทัดในไฟล์ settings.gradle เราสามารถทำให้ไฟล์ของเรา sync กันเมื่อเราทำการเปลี่ยนแปลงใน InnerLib
เมื่อเรา import InnerLib เข้าโปรเจกต์ Android Studio ทำสำเนาของ InnerLib และแคชมัน นั่นคือเหตุผลที่เราต้อง re-import ไลบรารีสำหรับทุกการเปลี่ยนแปลงที่เราทำภายในมัน เราสามารถบอก Android Studio ให้อ้างอิงไฟล์จากที่ไหนได้โดยใช้ attribute projectDir
settings.gradle ของเราอาจเป็นแบบนี้:
include ':outerLib', ':innerLib', ':app'
เพื่ออ้างอิง InnerLib ในท้องถิ่น เราต้องเปลี่ยน settings.gradle เป็น:
include ':outerLib', ':innerLib', ':app'
project('innerLib').projectDir = new File('PATH_TO_INNER_LIB')
ด้วยวิธีนี้ ไฟล์ InnerLib ของเราจะเชื่อมโยงกับ working directory ดังนั้นทุกการเปลี่ยนแปลงที่เราทำจะสะท้อนทันที
แต่ เราอยากมีความยืดหยุ่นเมื่อทำงานในท้องถิ่นกับ OuterLib พร้อม remote version ของ InnerLib สิ่งที่เราเขียนข้างบนใน settings.gradle จะทำให้เราทำงานในท้องถิ่นได้อย่างเดียว และแน่นอนเราไม่ต้องการ commit มันในสภาพนั้น
Maven Local
หากวิธีข้างบนไม่ค่อยถูกใจ มีอีกวิธีหนึ่ง คุณสามารถทำอย่างเดียวกับที่คุณจะเผยแพร่ไลบรารีสาธารณะด้วย maven คุณสามารถทำอย่างเดียวกันในท้องถิ่นด้วย maven local Maven local คือชุด repositories ที่อยู่ในเครื่องของคุณ
ด้านล่างคือ path สำหรับ mavenLocal ขึ้นอยู่กับ OS ของเครื่องคุณ:
- Mac → /Users/YOUR_USERNAME/.m2
- Linux → /home/YOUR_USERNAME/.m2
- Windows → C:\Users\YOUR_USERNAME\.m2
โดยปุ๋มคุณสามารถเผยแพร่ไลบรารีของคุณในท้องถิ่น และลิงก์มันในโปรเจกต์ของคุณ ทำแบบนี้ เราสามารถลิงก์โปรเจกต์ของเราไปที่ InnerLib
เพื่ออนุญาตให้การกำหนดค่านี้ในโปรเจกต์ของเรา เราต้องทำดังนี้:
- เพิ่ม mavenLocal() เป็น repository ภายใน clause repositories ของเรา นี่คือเพื่อให้โปรเจกต์ของเราสามารถค้นหา repositories ในท้องถิ่นได้
buildscript {
repositories {
mavenLocal()
}
}
...
allprojects {
repositories {
mavenLocal()
}
}
เปลี่ยนบรรทัด implementation ภายใน clause dependencies ให้อ้างอิง InnerLib เหมือนเรากำลังอ้างอิงมันแบบระยะไกล
เพื่อเผยแพร่ InnerLib ในท้องถิ่น เราจะสร้างไฟล์ชื่อ publishingLocally.gradle ที่จะมีสิ่งต่อไปนี้:
apply plugin: 'maven-publish'
project.afterEvaluate {
publishing {
publications {
library(MavenPublication) {
setGroupId groupId // your library package
setArtifactId artifactId
version versionName // I.E. 1.0
artifact bundleDebugAar
pom.withXml {
def dependenciesNode = asNode().appendNode('dependencies')
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', 'your_group_id')
dependencyNode.appendNode('artifactId', 'your_artificat_id')
dependencyNode.appendNode('version', 'your_version')
}
}
}
}
}
- ภายในไฟล์ build.gradle ระดับแอปพลิเคชัน เพิ่มบรรทัด:
apply from: '/.publishingLocally.gradle
หากตัวเลือกนี้ดูดีเกินจริง มันจริงๆ แล้วก็คืออย่างนั้น ในขณะที่ด้านหนึ่ง เราสามารถพัฒนาสิ่งต่างๆ ในท้องถิ่นได้อย่างราบรื่นเหมือนเรากำลังทำงานกับ remote library อีกด้านหนึ่ง หากเราทำการเปลี่ยนแปลงใดๆ ภายใน InnerLib ขณะทำงานในท้องถิ่น จำเป็นต้องเผยแพร่ในท้องถิ่นอีกครั้ง แม้จะไม่ใช่งานที่มีค่าใช้จ่ายสูง แต่ก็สร้างความจำเป็นต้องทำงานที่น่าเบื่อซ้ำๆ
โซลูชันสำหรับการทำงานในท้องถิ่นและระยะไกล
เราอยากหลีกเลี่ยงความจำเป็นต้อง re-publish InnerLib package ทุกครั้งที่เราทำการเปลี่ยนแปลงในท้องถิ่น เราต้องหาวิธีทำให้โปรเจกต์ของเรารู้จักการเปลี่ยนแปลงเหล่านั้น
ในส่วน Working Locally เราพบวิธีทำได้ แต่เรามีปัญหากับการ commit ไฟล์ settings.gradle เพื่อแก้ปัญหานี้ เพื่อให้เราสามารถทำงานทั้งในท้องถิ่นและระยะไกลกับ InnerLib ได้ เราจะใช้ parameter ที่เราจะกำหนดในไฟล์ gradle.properties
ไฟล์ gradle.properties เป็นที่ที่คุณเก็บการตั้งค่าระดับโปรเจกต์ที่กำหนดค่าสภาพแวดล้อมการพัฒนาของคุณ นี่ช่วยให้แน่ใจว่านักพัฒนาทุกคนในทีมมีสภาพแวดล้อมการพัฒนาที่สอดคล้องกัน
การตั้งค่าบางอย่างที่คุณอาจคุ้นเคยที่อยู่ในไฟล์นี้คือ AndroidX support (android.useAndroidX=true) หรือ JVM arguments (org.gradle.jvmargs=-Xmx1536m)
เพื่อช่วยแก้สถานการณ์ของเรา เราสามารถเพิ่ม parameter ที่นี่เพื่อระบุว่าเราอยากทำงานในท้องถิ่นหรือไม่ บางอย่างในลักษณะของ:
workingLocally = false
Parameter นี้จะให้เราสามารถแยกแยะได้ว่ากำลังทำงานกับ settings ไหน ไม่ว่าจะเป็นในท้องถิ่นหรือ production code ก่อน มาเปลี่ยนสิ่งที่เรามีใน settings.gradle โดยห่อหุ้มมันในเงื่อนไขที่ตรวจสอบว่า parameter ของเราเป็น true:
include ':outerLib', ':innerLib', ':app'
if (workingLocally.booleanValue()) {
project('innerLib').projectDir = new File('PATH_TO_INNER_LIB')
}
ด้วยวิธีนี้ เราบอกโปรเจกต์ให้ดึงไฟล์สำหรับ InnerLib ในท้องถิ่นจากเครื่องของเรา
อีกที่ที่เราต้องเปลี่ยนตรรกะคือในไฟล์ build.gradle ของเรา ที่นี่ แทนที่จะได้โค้ดไปที่ไลบรารีของเราแบบระยะไกลในบล็อก dependencies เราสามารถระบุได้ว่ากำลังขึ้นอยู่กับมันในท้องถิ่นหรือไม่
dependencies {
if (workingLocally.booleanValue()) {
implementation 'innerLib'
} else {
implementation 'url_to_remote_repository'
}
}
⚠️ คำเตือน: คุณไม่ควร commit ไฟล์ gradle.properties เมื่อทำงานในท้องถิ่น
การเดินทางยาวนานและอาจดูเหมือนเหนื่อยล้า แต่ตอนนี้เรามีการตั้งค่าที่ fool-proof สำหรับการทำงานในท้องถิ่นและระยะไกลบนโปรเจกต์หลายไลบรารี
หากคุณเจอปัญหาใดๆ หรืออยากให้ความคิดเห็น รู้สึกว่าอิสระที่จะทิ้งความคิดเห็นไว้
เรียนรู้การเขียนโค้ดฟรี หลักสูตรโอเพนซอร์สของ freeCodeCamp ช่วยให้ผู้คนมากกว่า 40,000 คนได้งานเป็นนักพัฒนา เริ่มต้นเลยวันนี้