นักพัฒนาแอนดรอยด์แทบทุกคนล้วนมี Library เป็นที่พึ่งทางใจในเวลาทำงาน ซึ่งเจ้าของบล็อกก็เป็นหนึ่งในนั้นเหมือนกัน แต่ทว่าเวลาทำงานจริงนั้นก็อาจจะต้องเจอปัญหา Conflict ระหว่าง Dependency บางตัว มาดูกันว่าปกติแล้วเจ้าของบล็อกจัดการกับปัญหาดังกล่าวยังไง
เบื่อมั้ยกับป้ญหาแบบนี้?
ยกตัวอย่างเช่น เจ้าของบล็อกเรียกใช้งานไลบรารีที่ชื่อว่า Smart Location ที่เป็นไลบรารีสำหรับเรียกใช้งาน Location Service โดยที่ตัวไลบรารีรองรับการเรียกใช้งาน Location API ของ Google Play Services ด้วยdependencies {
...
compile 'io.nlopez.smartlocation:library:3.2.9'
}
อาจจะดูเหมือนไม่มีอะไร และเรียกใช้งานได้ปกติสุขดี
แต่สมมติว่าวันหนึ่ง แอปฯที่ใช้ไลบรารีตัวนี้เกิดต้องใช้ FCM (Firebase Cloud Messaging) ขึ้นมา ก็เลยเพิ่ม Dependency เข้าไปแบบนี้
dependencies {
...
compile 'io.nlopez.smartlocation:library:3.2.9'
compile 'com.google.firebase:firebase-messaging:10.0.1'
}
ถ้า Build Gradle เฉยๆก็อาจจะไม่มีปัญหาอะไรเกิดขึ้น แต่เมื่อกด Run เพื่อทำเป็น APK แล้วติดตั้งลงในอุปกรณ์แอนดรอยด์ ก็จะเกิดปัญหา Conflict แล้วแสดงข้อความใน Message ของ Gradle แบบนี้
ดูเผินๆก็นึกว่าเป็นปัญหาเรื่อง MultiDex แต่ที่จริงแล้วปัญหาที่เกิดขึ้นนั้นคือมี Dependency บางชุดที่เรียกใช้งานซ้ำซ้อนกัน
ซ้ำซ้อนกันยังไง?
ลองคิดๆดูแล้ว Smart Location กับ FCM ไม่น่าจะมีอะไรที่ Conflict กันนี่นา?
มาดูวิธีหาสาเหตุกัน
อยากรู้ว่า Conflict มั้ยให้เช็คด้วยคำสั่งผ่าน Terminal ดังนี้ (ตัวอย่างนี้เป็นคำสั่งบน macOS)// Linux และ Mac OS
./gradlew app:dependencies
// Windows
gradlew app:dependencies
ซึ่งคำสั่งนี้เป็นคำสั่งสำหรับเช็ค Dependency ที่เรียกใช้งานในโปรเจค โดยจะได้ผลลัพธ์ออกมาประมาณนี้
Desktop/DemoApp Akexorcist$ ./gradlew app:dependencies
To honour the JVM settings for this build a new JVM will be forked. Please consider using the daemon: https://docs.gradle.org/2.14.1/userguide/gradle_daemon.html.
Incremental java compilation is an incubating feature.
:app:dependencies
------------------------------------------------------------
Project :app
------------------------------------------------------------
...
_debugApk - ## Internal use, do not manually configure ##
+--- com.android.support:appcompat-v7:25.0.1
| +--- com.android.support:support-v4:25.0.1
| | +--- com.android.support:support-compat:25.0.1
| | | \--- com.android.support:support-annotations:25.0.1
| | +--- com.android.support:support-media-compat:25.0.1
| | | \--- com.android.support:support-compat:25.0.1 (*)
| | +--- com.android.support:support-core-utils:25.0.1
| | | \--- com.android.support:support-compat:25.0.1 (*)
| | +--- com.android.support:support-core-ui:25.0.1
| | | \--- com.android.support:support-compat:25.0.1 (*)
| | \--- com.android.support:support-fragment:25.0.1
| | +--- com.android.support:support-compat:25.0.1 (*)
| | +--- com.android.support:support-media-compat:25.0.1 (*)
| | +--- com.android.support:support-core-ui:25.0.1 (*)
| | \--- com.android.support:support-core-utils:25.0.1 (*)
| +--- com.android.support:support-vector-drawable:25.0.1
| | \--- com.android.support:support-compat:25.0.1 (*)
| \--- com.android.support:animated-vector-drawable:25.0.1
| \--- com.android.support:support-vector-drawable:25.0.1 (*)
+--- io.nlopez.smartlocation:library:3.2.9
| +--- com.google.android.gms:play-services-location:9.8.0
| | +--- com.google.android.gms:play-services-base:9.8.0
| | | +--- com.google.android.gms:play-services-basement:9.8.0 -> 10.0.1
| | | | \--- com.android.support:support-v4:24.0.0 -> 25.0.1 (*)
| | | \--- com.google.android.gms:play-services-tasks:9.8.0 -> 10.0.1
| | | \--- com.google.android.gms:play-services-basement:10.0.1 (*)
| | \--- com.google.android.gms:play-services-basement:9.8.0 -> 10.0.1 (*)
| \--- com.android.support:support-annotations:25.0.0 -> 25.0.1
\--- com.google.firebase:firebase-messaging:10.0.1
+--- com.google.android.gms:play-services-basement:10.0.1 (*)
+--- com.google.firebase:firebase-iid:10.0.1
| +--- com.google.android.gms:play-services-basement:10.0.1 (*)
| \--- com.google.firebase:firebase-common:10.0.1
| +--- com.google.android.gms:play-services-basement:10.0.1 (*)
| \--- com.google.android.gms:play-services-tasks:10.0.1 (*)
\--- com.google.firebase:firebase-common:10.0.1 (*)
...
BUILD SUCCESSFUL
Total time: 12.053 secs
จริงๆผลลัพธ์ที่ได้จะยาวกว่านี้มาก แต่เจ้าของบล็อกขอตัดให้เหลือเฉพาะส่วนที่จำเป็นจริงๆ คือ debugApk หรือ releaseApk
...
+--- io.nlopez.smartlocation:library:3.2.9
| +--- com.google.android.gms:play-services-location:9.8.0
| | +--- com.google.android.gms:play-services-base:9.8.0
| | | +--- com.google.android.gms:play-services-basement:9.8.0 -> 10.0.1
| | | | \--- com.android.support:support-v4:24.0.0 -> 25.0.1 (*)
| | | \--- com.google.android.gms:play-services-tasks:9.8.0 -> 10.0.1
| | | \--- com.google.android.gms:play-services-basement:10.0.1 (*)
| | \--- com.google.android.gms:play-services-basement:9.8.0 -> 10.0.1 (*)
| \--- com.android.support:support-annotations:25.0.0 -> 25.0.1
...
ถ้าลองดูในส่วนของ io.nlopez.smarlocation (ของ Smart Location) จะเห็นว่าข้างในไลบรารีตัวนี้มีการเรียกใช้ Location API ของ Google Play Services กับ Annotation ของ Android Support แต่ถ้าโฟกัสเฉพาะ Location API ก็จะเห็นว่าข้างในนั้นมีการเรียกใช้ Basement ของ Google Play Services อยู่ด้วย ทั้งนี้ก็เพราะว่า Google Play Services มีการแยกไลบรารีออกเป็นหลายชุดออกจากกันเพื่อไม่ให้มีขนาดใหญ่และหนักเกินไป แต่ไลบรารีแต่ละชุดของ Google Play Services ก็ต้องมีไลบรารีกลางที่ใช้งานร่วมกันซึ่งเค้าตั้งชื่อว่า Basement นั่นเอง
และพอลองดูของ Firebase Cloud Messaging หรือ (FCM) ก็จะเป็นแบบนี้
...
\--- com.google.firebase:firebase-messaging:10.0.1
+--- com.google.android.gms:play-services-basement:10.0.1 (*)
+--- com.google.firebase:firebase-iid:10.0.1
| +--- com.google.android.gms:play-services-basement:10.0.1 (*)
| \--- com.google.firebase:firebase-common:10.0.1
| +--- com.google.android.gms:play-services-basement:10.0.1 (*)
| \--- com.google.android.gms:play-services-tasks:10.0.1 (*)
\--- com.google.firebase:firebase-common:10.0.1 (*)
...
สังเกตเห็นอะไรมั้ย?
FCM ก็มีการเรียกใช้งาน Basement ของ Google Play Services เหมือนกัน (เพราะ Firebase ไปครอบการทำงานของ Google Play Services อีกที) แต่ทว่าเป็น Basement คนละเวอร์ชันกัน
นั่นล่ะครับ ปัญหาที่เกิดขึ้น เพราะว่าไลบรารีทั้ง 2 ตัวของเจ้าของบล็อกใช้ Basement เหมือนกัน แต่ทว่าดันเป็นคนละเวอร์ชันกัน (Smart Location ใช้เวอร์ชัน 9.8.0 ส่วน FCM ใช้เวอร์ชัน 10.0.1)
แก้ปัญหายังไงดี?
วิธีที่โอเคและถูกต้องที่สุดคือไลบรารีทั้ง 2 ตัวควรใช้เวอร์ชันเดียวกันและเป็นเวอร์ชันล่าสุด แต่เนื่องจาก Smart Location ของเจ้าของบล็อกดันกำหนดตายตัวไว้เป็นเวอร์ชัน 9.8.0 จึงต้องหาทางยังไงก็ได้ให้ไปใช้เป็นเวอร์ชัน 10.0.1 แต่ถ้า Basement เป็นเวอร์ชัน 10.0.1 ดังนั้น Location API ก็ควรเป็น 10.0.1 ตามด้วย ดังนั้นเป้าหมายคือทำให้ Smart Location ใช้ Location API เป็นเวอร์ชัน 10.0.1 เหมือนกับ FCM
ลบของเวอร์ชันเก่าออกซะ
ในเมื่อเข้าไปแก้ไขอะไรไม่ได้ ก็ต้องทำผ่าน build.gradle แทน เพื่อเอา Location API ออกซะ โดยใช้ Exclude เข้ามาช่วย
...
dependencies {
...
compile('io.nlopez.smartlocation:library:3.2.9') {
exclude module: 'play-services-location'
}
compile 'com.google.firebase:firebase-messaging:10.0.1'
}
คำสั่ง Exclude จะทำให้เจ้าของบล็อกสามารถลบ Location API ที่อยู่ใน Smart Location ได้ โดยกำหนดผ่านชื่อ Module ของ Location API ลงไป
หรือจะ Exclude เป็น Group ไปเลยก็ได้นะ
...
dependencies {
...
compile('io.nlopez.smartlocation:library:3.2.9') {
exclude group: 'com.google.android.gms'
}
compile 'com.google.firebase:firebase-messaging:10.0.1'
}
แต่การ Exclude แบบ Group ต้องระวังด้วยว่าจะไม่ไปกระทบกับ Dependency ตัวอื่นที่อยู่ข้างในที่มีชื่อ Group เหมือนกัน เพราะไม่งั้นจะลบไปด้วย
อยากรู้ว่าลบออกไปแล้วจริงๆหรือป่าว ก็ลองรันทดสอบดูได้ เพราะมันจะ Comple เป็น APK ได้ แต่ว่าเวลาเรียกใช้งาน Smart Location จะทำงานไม่ได้ เพราะ Dependency ตัวดังกล่าวถูกลบทิ้งไป
เพิ่มของเวอร์ชันล่าสุดเข้าไป
ทำการเพิ่ม Location API ที่เป็นเวอร์ชัน 10.0.1 (เวอร์ชันล่าสุดที่เขียนบทความนี้)
...
dependencies {
...
compile('io.nlopez.smartlocation:library:3.2.9') {
exclude module: 'play-services-location'
}
compile 'com.google.firebase:firebase-messaging:10.0.1'
compile 'com.google.android.gms:play-services-location:10.0.1'
}
เจ้า Smart Location ก็จะเรียกใช้ Location API จากตัวที่เจ้าของบล็อกเพิ่มเข้าไปให้ทันที และสามารถทำงานได้เลย
เท่านี้เจ้าของบล็อกก็สามารถใช้งาน Smart Location กับ FCM ร่วมกันได้แล้ว
เรื่องที่ควรรู้
• วิธีแก้ปัญหาจะขึ้นอยู่กับแต่ละกรณี เพราะงั้นบางกรณีก็อาจจะต้องแก้ปัญหาแตกต่างกันไป• การแทนที่ด้วย Dependency ที่เวอร์ชันใหม่กว่า จะทำได้ก็ต่อเมื่อโค้ดข้างในไม่ได้แตกต่างกันมากนักในระดับ Minor Change เพราะถ้าโค้ดทั้งสองเวอร์ชันมีความแตกต่างกันมากจนถึงระดับคำสั่งบางอย่างถูกเอาออกไป ก็อาจจะทำให้ไลบรารีทำงานไม่ได้เลย
• คำสั่ง ./gradlew app:dependencies มีประโยชน์โคตรๆ เพราะนอกจากดู Dependency ที่ใช้งานเพื่อวิเคราะห์ได้ว่าตัวไหนจำเป็น ตัวไหนไม่จำเป็น มันยังบอกได้อีกว่าตัวไหนสามารถอัพเดทเป็นเวอร์ชันใหม่กว่าได้
\--- com.google.android.gms:play-services-basement:9.8.0 -> 10.0.1 (*)
จากตัวอย่างคือเรียกใช้เวอร์ชัน 9.8.0 อยู่ แต่สามารถอัปเดตเป็น 10.0.1 ได้