Android resource linking failed เป็นหนึ่งในปัญหาที่นักพัฒนาสามารถพบเจอได้เป็นบางครั้ง เจ้าของบล็อกจึงเขียนบทความนี้ไว้เพื่อช่วยให้นักพัฒนาสามารถวิเคราะห์ปัญหาและแก้ไขได้อย่างเหมาะสม
สมมติว่าเจ้าของบล็อกมีโปรเจคตัวหนึ่งที่กำหนดค่าไว้ใน build.gradle (module: app)
ไว้แบบนี้
// build.gradle
android {
/* ... */
defaultConfig {
targetSdk 30
/* ... */
}
}
dependencies {
/* ... */
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'com.markodevcic:peko:2.2.0'
}
โดยจุดสังเกตคือ โปรเจคดังกล่าวยังรองรับแค่ API Level 30 อยู่ และมีการเรียกใช้งาน Library ของ AndroidX กับ Material Design รวมไปถึง Library ที่ชื่อว่า Peko
คำสั่งดังกล่าวจะไม่มีปัญหาตอนที่ Sync Gradle แต่จะเกิดขึ้นในตอนที่กดปุ่ม Run App และจะแสดงเป็นข้อความแบบนี้แทน
อ้าว ทำไมถึงมีปัญหาได้ล่ะ?
Android resource linking failed เกิดจากอะไร?
ปัญหาดังกล่าวเกิดจากตอนที่ AAPT ไม่สามารถรวม Android Resource ทั้งหมดเข้าด้วยกันได้นั่นเอง โดยที่ AAPT จะทำงานเฉพาะตอนที่ Build AAB/APK เท่านั้น จึงเป็นที่มาทำไมปัญหานี้ไม่เจอตั้งแต่ตอน Sync Gradle
และสาเหตุหลัก ๆ ของปัญหาดังกล่าวก็เกิดมาจาก Library บางตัวในโปรเจคมี Dependency เหมือนกันแต่เป็นเวอร์ชันต่างกัน และมี Target SDK ไม่ตรงกับโปรเจค
Android resource linking failed
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:3: error: resource android:color/system_neutral1_1000 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:4: error: resource android:color/system_neutral1_900 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:5: error: resource android:color/system_neutral1_0 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:6: error: resource android:color/system_neutral1_800 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:7: error: resource android:color/system_neutral1_700 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:8: error: resource android:color/system_neutral1_600 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:9: error: resource android:color/system_neutral1_500 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:10: error: resource android:color/system_neutral1_400 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:11: error: resource android:color/system_neutral1_300 not found.
com.akexorcist.devfest2022.app-mergeDebugResources-17:/values-v31/values-v31.xml:12: error: resource android:color/system_neutral1_200 not found.
...
เมื่อลองดูข้อความดังกล่าวก็จะพบว่าปัญหาเกิดมาจากไฟล์ที่อยู่ใน values-v31
ที่เป็นของ API Level 31 ในขณะที่โปรเจคยังกำหนด Target SDK เป็น API Level 30 อยู่ จึงสามารถสรุปคร่าว ๆ ได้ว่ามี Library บางตัวที่ใหม่เกินไป
ปัญหาดังกล่าวเกิดได้บ่อยกับโปรเจคเก่าที่เพิ่ม Library ใหม่เข้าไป แล้วไม่สัมพันธ์กับเวอร์ชันของ Library ตัวอื่น ๆ ที่มีอยู่ในโปรเจค
ดังนั้นเรามาหากันว่าเป็นเพราะ Library ตัวไหน
ตามหา Library เจ้าปัญหา
โดยปกติแล้วถ้า Library ที่ใช้เป็นเวอร์ชันที่กำหนด Target SDK สูงกว่าในโปรเจค ก็จะพังตั้งแต่ Sycn Gradle ทันที แต่สำหรับปัญหา Android resource linking failed จะเกิดมาจาก Dependency ที่อยู่ใน Library เหล่านั้นอีกที
ดังนั้นเพื่อเช็ค Dependency ที่มีอยู่ใน Library แต่ละตัว ให้ลองใช้คำสั่ง gradle app:dependencies
ของ Gradle เพื่อดูว่าใน Library แต่ละตัวมี Dependency อะไรบ้างและเวอร์ชันไหน
หรือจะใช้คำสั่ง ./gradlew app:dependencies
ใน Terminal ก็ได้
คำสั่งดังกล่าวจะแสดง Dependency Tree ในโปรเจค ให้เราดูเฉพาะตรง debugCompileClasspath
ก็พอ
และจากตัวอย่างใน build.gradle
ก่อนหน้า จะได้ออกมาประมาณนี้
debugCompileClasspath - Compile classpath for compilation 'debug' (target (androidJvm)).
+--- androidx.core:core-ktx:1.5.0
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.31 -> 1.7.10 (*)
| +--- androidx.annotation:annotation:1.1.0 -> 1.2.0
| \--- androidx.core:core:1.5.0
+--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0
| +--- androidx.annotation:annotation:1.1.0 -> 1.2.0
| +--- androidx.core:core:1.3.0 -> 1.5.0 (*)
| +--- androidx.cursoradapter:cursoradapter:1.0.0
| +--- androidx.fragment:fragment:1.1.0 -> 1.2.0
| +--- androidx.appcompat:appcompat-resources:1.2.0
| \--- androidx.drawerlayout:drawerlayout:1.0.0 -> 1.1.1
+--- com.google.android.material:material:1.4.0 -> 1.5.0
| +--- androidx.annotation:annotation:1.2.0
| +--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*)
| +--- androidx.cardview:cardview:1.0.0
| +--- androidx.coordinatorlayout:coordinatorlayout:1.1.0
| +--- androidx.constraintlayout:constraintlayout:2.0.1 -> 2.1.4
| +--- androidx.core:core:1.5.0 (*)
| +--- androidx.drawerlayout:drawerlayout:1.1.1 (*)
| +--- androidx.dynamicanimation:dynamicanimation:1.0.0
| +--- androidx.annotation:annotation-experimental:1.0.0
| +--- androidx.fragment:fragment:1.0.0 -> 1.2.0 (*)
| +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.2.0 (*)
| +--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0
| +--- androidx.transition:transition:1.2.0
| +--- androidx.vectordrawable:vectordrawable:1.1.0 (*)
| \--- androidx.viewpager2:viewpager2:1.0.0
+--- androidx.constraintlayout:constraintlayout:2.1.4
+--- com.markodevcic:peko:2.2.0
| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.30 -> 1.7.10 (*)
| +--- androidx.lifecycle:lifecycle-extensions:2.2.0
| +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2
| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2(*)
| \--- com.google.android.material:material:1.5.0 (*)
+--- androidx.core:core-ktx:{strictly 1.5.0} -> 1.5.0 (c)
...
ในความเป็นจริงจะได้ Dependency Tree ที่ยาวและเยอะกว่านี้มาก แต่ขอตัดบางส่วนออก เพื่อให้ดูต้นเหตุของปัญหาได้ง่ายขึ้น
ถึงแม้ Dependency Tree ที่ได้อาจจะหลายบรรทัดไปหน่อย แต่อยากให้ลองสังเกตตรง Library ที่ชื่อ Peko (com.markodevcic:peko
) จะเห็นว่าข้างใน Library ดังกล่าวใช้ Material Design (com.google.android.material:material
) เวอร์ชัน 1.5.0 แต่ที่กำหนดไว้ในโปรเจคเป็นเวอร์ชัน 1.4.0
ดังนั้นในกรณีนี้ให้สงสัย Library ดังกล่าวไว้ก่อน พร้อมกับหาข้อมูลใน Release Note ของ Material Design ว่าเวอร์ชัน 1.5.0 มีการเปลี่ยน Targer SDK เป็น API Level 31 หรือไม่
ณ จุดนี้ต้องหา Release Note ของ Library ตัวนั้นให้เจอ
และเมื่อดู Release Note ของ Material Design เวอร์ชัน 1.5.0 ก็พบว่ามีการเปลี่ยน Target SDK เป็น API Level 31 จริง
ดังนั้นในกรณีนี้เจ้าของบล็อกรู้แล้วว่าเป็นเพราะ Peko ใช้ Material Design เวอร์ชันใหม่เกินไป และไม่น่าจะมี Library ตัวอื่นที่เป็นต้นเหตุเหมือนกับ Peko แล้ว
คราวนี้ก็ถึงเวลาแก้ปัญหา ซึ่งจะมีวิธีแก้ปัญหาอยู่ 3 วิธีด้วยกัน โดยขึ้นอยู่กับสถานการณ์ในแต่ละโปรเจค
วิธีที่ 1 - Downgrade Library Version ให้สัมพันธ์กับ Library ตัวอื่น ๆ ในโปรเจค
ที่แนะนำวิธีนี้เป็นวิธีแรกสุด เพราะเป็นวิธีที่สามารถทำได้ง่ายที่สุดสำหรับโปรเจคส่วนใหญ่ที่มีความซับซ้อนและมีการใช้ Library เยอะ และไม่อยากเสียเวลากับปัญหาดังกล่าวมากนัก
แต่ถ้าเป็นไปได้ก็ให้ใช้วิธีที่ 2 นะ
โดยให้นักพัฒนาเช็คจาก Release Note ของ Library ตัวนั้นว่ามีเวอร์ชันไหนบ้างที่เวอร์ชันของ Dependency ที่มีปัญหาตรงกับที่ใช้งานในโปรเจค
จากตัวอย่างในโปรเจคเจ้าของบล็อก จะต้องหาเวอร์ชันที่กำหนด Targer SDK เป็น API Level 30 และใช้ Material Design เป็นเวอร์ชัน 1.4.0 นั่นเอง
เมื่อลองเข้าไปส่อง Code Changes ใน GitHub ของ Library ดังกล่าว ก็พบว่าเจ้าของบล็อกโชคดีมากที่ Peko ทำการอัปเดต Material Design และ Target SDK พร้อมกันในเวอร์ชัน 2.2.0
ดังนั้นกรณีนี้จึงง่ายมาก เพราะให้ย้อนกลับไปใช้เวอร์ชันที่เก่ากว่าหนึ่งเวอร์ชันก็พอ และเมื่อเช็คจากหน้า Release ของ Peko ก็จะพบว่าเวอร์ชันก่อนหน้านั้นคือ 2.1.4
// build.gradle
android {
/* ... */
defaultConfig {
targetSdk 30
/* ... */
}
}
dependencies {
/* ... */
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'com.markodevcic:peko:2.1.4'
}
จากนั้นก็ Sync Gradle, Run App, ภาวนาให้ Gradle สามารถ Build AAB/APK ได้อย่างไม่มีปัญหา และแอปสามารถทำงานได้อย่างที่ควรจะเป็น
แต่วิธีนี้อาจจะใช้ไม่ได้ผล ถ้า Downgrade แล้วทำให้ Dependency บางตัวมีเวอร์ชันไม่ตรงกับ Library ตัวอื่น ๆ (หนีเสือปะจระเข้) หรือ Library ไม่มีเวอร์ชันที่ต่ำกว่านั้นแล้ว ก็อาจจะต้องยอมเปลี่ยนไปใช้วิธีที่ 2 แทน
วิธีที่ 2 - อัปเดตโปรเจคให้ใช้ Target SDK ตามที่ Library ต้องการ
วิธีนี้จะเหมาะกับโปรเจคที่พร้อมสำหรับการอัปเดต Target SDK เท่านั้น เพราะโดยปกติแล้วการอัปเดต Target SDK ไม่ใช่แค่การแก้ไขตัวเลขเท่านั้น แต่รวมไปถึงการแก้โค้ดให้เข้ากับ Behavior Changes ของแต่ละ API Level จึงทำให้บางโปรเจคไม่เหมาะกับวิธีนี้ใน ณ เวลานั้น ๆ
ในกรณีที่สามารถทำได้ ก็ให้เปลี่ยน Target SDK ตามที่ Library ตัวนั้นต้องการ และเพื่อให้ Library ตัวอื่น ๆ ในโปรเจคสามารถทำงานบน Target SDK ตัวใหม่ได้ถูกต้องเหมือนเดิม ให้ลองตรวจสอบดูว่า Library เหล่านั้นมีเวอร์ชันใหม่ที่รองรับ Target SDK ตามที่เรากำหนดด้วยหรือไม่
จากตัวอย่างจะได้ build.gradle
เป็นแบบนี้แทน
// build.gradle
android {
/* ... */
defaultConfig {
targetSdk 31
/* ... */
}
}
dependencies {
/* ... */
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.markodevcic:peko:2.2.0'
}
สำหรับ Library ของ AndroidXให้เช็ค Target SDK จาก AndroidX Tech
เมื่อไล่เช็คและอัปเดตจนครบหมดทุกตัวแล้ว ให้ลอง Run App และทดสอบการทำงานดูทั้งหมดอีกครั้งเพื่อยืนยันว่าแอปสามารถทำงานได้ถูกต้องเหมือนเดิม
วิธีที่ 3 - Fork เพื่อทำเป็น Internal Library
เจ้าของบล็อกไม่ค่อยแนะนำวิธีนี้เท่าไร เพราะเป็นการเอา Library ของคนอื่นมาแก้เพื่อใช้งานส่วนตัว แต่บ่อยครั้งวิธีนี้ก็ตอบโจทย์กว่าวิธีที่ 1 และ 2 ด้วยเวลาและข้อจำกัดอื่น ๆ และการแก้ไขโค้ดจะทำเพื่อให้รองรับกับโปรเจคของตัวเองเท่านั้น จึงไม่สามารถเปิด Pull Request ให้กับ Repository ต้นทางได้
สรุป
Android resource linking failed เกิดจาก AAPT ไม่สามารถรวม Android Resource ในโปรเจคได้เนื่องจากมี Library บางตัวที่ใช้ Dependencies ต่างกับ Library ตัวอื่น ๆ และมี Target SDK ต่างกัน จึงทำให้โปรเจคของเราไม่รู้จักกับ Android Resource ที่เพิ่มเข้ามาใน API Level นั้น ๆ และทำให้ไม่สามารถ Build AAB/APK ได้
ในการแก้ไขปัญหาดังกล่าวจะทำได้หลายวิธี โดยขึ้นอยู่กับสถานการณ์ของนักพัฒนาเป็นหลัก เพราะแต่ละขั้นตอนก็มีข้อดีข้อเสียแตกต่าง เช่น วิธีแก้ไขที่ถูกต้องที่สุดก็มาพร้อมกับการใช้เวลาแก้ไขที่นานที่สุด หรือวิธีแก้ไขที่เร็วที่สุดก็เป็นวิธีที่ใช้ได้ไม่นาน พอถึงจุดหนึ่งก็อาจจะเจอปัญหาอื่นเพิ่มเข้ามาแทน เป็นต้น
ดังนั้นพิจารณาให้ดี แล้วเลือกวิธีแก้ไขปัญหาที่เหมาะกับโปรเจคของคุณครับ