นักพัฒนาแอนดรอยด์แทบทุกคนล้วนมี Library เป็นที่พึ่งทางใจในเวลาทำงาน ซึ่งเจ้าของบล็อกก็เป็นหนึ่งในนั้นเหมือนกัน แต่ทว่าเวลาทำงานจริงนั้นก็อาจจะต้องเจอปัญหา Conflict ระหว่าง Dependency บางตัว มาดูกันว่าปกติแล้วเจ้าของบล็อกจัดการกับปัญหาดังกล่าวยังไง

เบื่อมั้ยกับป้ญหาแบบนี้?

ยกตัวอย่างเช่น เจ้าของบล็อกเรียกใช้งาน Library ที่ชื่อว่า Smart Location สำหรับเรียกใช้งาน Location Service โดยที่ Library รองรับการเรียกใช้งาน Location API ของ Google Play Services ด้วย

dependencies {
    ...
    implementation 'io.nlopez.smartlocation:library:3.2.9'
}

อาจจะดูเหมือนไม่มีอะไร และเรียกใช้งานได้ปกติสุขดี

แต่สมมติว่าวันหนึ่ง แอปฯที่ใช้ไลบรารีตัวนี้เกิดต้องใช้ FCM (Firebase Cloud Messaging) ขึ้นมา ก็เลยเพิ่ม Dependency เข้าไปแบบนี้

dependencies {
    /* ... */
    implementation 'io.nlopez.smartlocation:library:3.2.9'
    implementation '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 เข้ามาช่วย

// build.gradle
dependencies {
    /* ... */
    implementation('io.nlopez.smartlocation:library:3.2.9') {
        exclude module: 'play-services-location'
    }
    implementation 'com.google.firebase:firebase-messaging:10.0.1'
}

คำสั่ง Exclude จะทำให้เจ้าของบล็อกสามารถลบ Location API ที่อยู่ใน Smart Location ได้ โดยกำหนดผ่านชื่อ Module ของ Location API ลงไป

หรือจะ Exclude เป็น Group ไปเลยก็ได้นะ

// build.gradle
dependencies {
    /* ... */
    implementation('io.nlopez.smartlocation:library:3.2.9') {
        exclude group: 'com.google.android.gms'
    }
    implementation 'com.google.firebase:firebase-messaging:10.0.1'
}

แต่การ Exclude แบบ Group ต้องระวังด้วยว่าจะไม่ไปกระทบกับ Dependency ตัวอื่นที่อยู่ข้างในที่มีชื่อ Group เหมือนกัน เพราะไม่งั้นจะลบไปด้วย

อยากรู้ว่าลบออกไปแล้วจริงๆหรือป่าว ก็ลองรันทดสอบดูได้ เพราะมันจะ Comple เป็น APK ได้ แต่ว่าเวลาเรียกใช้งาน Smart Location จะทำงานไม่ได้ เพราะ Dependency ตัวดังกล่าวถูกลบทิ้งไป

เพิ่มของเวอร์ชันล่าสุดเข้าไป

ทำการเพิ่ม Location API ที่เป็นเวอร์ชัน 10.0.1 (เวอร์ชันล่าสุดที่เขียนบทความนี้)

// build.gradle
dependencies {
    /* ... */
    implementation('io.nlopez.smartlocation:library:3.2.9') {
        exclude module: 'play-services-location'
    }
    implementation 'com.google.firebase:firebase-messaging:10.0.1'
    implementation '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 ได้

สรุป

Dependency Conflict เป็นเรื่องที่เจอกันได้เป็นบางครั้ง เพราะว่าไลบรารีหลายๆตัวก็ไม่ได้อัปเดต Dependency ที่ใช้งานให้เป็นเวอร์ชันล่าสุดเสมอไป ดังนั้นนักพัฒนาอย่างเราๆก็ควรรู้ว่าไลบรารีที่ใช้งานอยู่นั้นเป็นยังไง เรียกใช้ Dependency อะไรบ้าง เพื่อที่ว่ามีปัญหาจะได้หาสาเหตุได้ง่ายขึ้น แก้ปัญหาได้ง่าย รวมไปถึงรู้ถึงปัญหาโปรเจคที่บวมเกินจำเป็นเพราะเรียกใช้ไลบรารีเยอะเกินไปด้วยนะเออ