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 จริง

Release 1.5.0 · material-components/material-components-android
What’s new since 1.4.0 Material3 themes, styles, and functionality! Check out the following resources for more information: Material 3 site and guidelinesStart building with Material You blog po...

ดังนั้นในกรณีนี้เจ้าของบล็อกรู้แล้วว่าเป็นเพราะ 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

bump all library versions to latest · deva666/Peko@ce34289
Android Library for requesting Permissions with Kotlin Coroutines or AndroidX LiveData - bump all library versions to latest · deva666/Peko@ce34289

ดังนั้นกรณีนี้จึงง่ายมาก เพราะให้ย้อนกลับไปใช้เวอร์ชันที่เก่ากว่าหนึ่งเวอร์ชันก็พอ และเมื่อเช็คจากหน้า 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 ได้

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

ดังนั้นพิจารณาให้ดี แล้วเลือกวิธีแก้ไขปัญหาที่เหมาะกับโปรเจคของคุณครับ