Non-Transitive R Classes เป็นหนึ่งในการทำงานของ Android Gradle Plugin ที่ถูกเพิ่มเข้ามาตั้งแต่เวอร์ชัน 4.1 เพื่อช่วยลด Build Time ให้เร็วขึ้น โดยที่นักพัฒนาจะต้องแก้ไขโปรเจคของตนเองให้เข้ากับการทำงานดังกล่าวด้วย
R Class คืออะไร?
เมื่อนักพัฒนาต้องการเรียกใช้งาน Android Resource ใด ๆ ก็ตามผ่านโค้ด Java หรือ Kotlin ก็ตาม ก็จะต้องเรียกผ่านคลาสนี้เสมอ ซึ่งเป็นคลาสที่เป็นตัวกลางที่จะเชื่อมระหว่างโค้ดของเรากับข้อมูลที่อยู่ใน Android Resource เข้าด้วยกัน
R.drawable.ic_home
R.string.hello_world
R.color.purple_700
R.anim.fade_in
R.layout.activity_main
ซึ่ง R Class จะถูกสร้างขึ้นโดย AAPT ในตอน Compile Time เพื่อสร้าง ID ให้กับ Android Resource ทุกตัวที่มีอยู่ในโปรเจค
AAPT หรือ Android Asset Packaging Tool เป็น Build Tool ที่ทำหน้านี้จัดการและรวมไฟล์ต่าง ๆ สำหรับแอปแอนดรอยด์ในระหว่าง Compile Time
public final class R {
private R() {}
public static final class anim {
public static final int fade_in = 0x7f01001d
}
public static final class color {
public static final int purple_700 = 0x7f050000
}
public static final class drawable {
public static final int ic_home = 0x7f070087
}
public static final class layout {
public static final int activity_main = 0x7f0b0068
}
public static final class string {
public static final int hello_world = 0x7f0f0035
}
}
เนื่องจาก ID ดังกล่าวถูกสร้างขึ้นมาเป็น Integer จึงเป็นที่มาว่าคำสั่งที่เกี่ยวกับ Android Resource จะต้องรับค่า ID เป็น Integer เสมอ
fun setTitle(@StringRes resId: Int) { /* ... */ }
setTitle(R.string.hello_world)
ปัญหาจาก Transitive R Classes
เดิมทีนั้น AAPT จะใช้วิธีที่เรียกว่า Transitive R Classes มาโดยตลอด โดยที่ R Class ในทุก Module จะรวมไปถึง Android Resource จาก Module ที่เป็น Dependency ด้วย
ยกตัวอย่างเช่น ในโปรเจคมีทั้งหมด 3 Module และแต่ละ Module ก็มี String Resource เป็นของตัวเอง โดยมี Dependency Graph แบบนี้
เมื่อ AAPT สร้าง R Class ให้กับแต่ละ Module ก็จะเอา Android Resource ของ Dependency ในแต่ละ Module มาด้วย จึงได้ผลลัพธ์เป็นแบบนี้
จากตัวอย่างจะเห็นว่า :feature
ได้ String Resource ของ :base
มาด้วย และในขณะเดียวกัน :app
ที่เป็น Top-level module ก็มี String Resource ที่มาจากทั้ง :feature
และ :base
Transitive R Classes จะสะดวกตรงที่แต่ละ Module สามารถเรียก Android Resource จาก R Class ของตัวเองได้เลย เพราะ AAPT รวมไว้ให้เรียบร้อยแล้ว
// app module
import com.sfl.R
R.string.app_name
R.string.home_menu
R.string.confirm
// feature module
import com.sfl.feature.R
R.string.home_menu
R.string.confirm
// base module
import com.sfl.base.R
R.string.confirm
โดยปัญหาของ Transitive R Classes ก็คือเมื่อโปรเจคมีขนาดใหญ่มากขึ้นจน Android Resource ในโปรเจคมีเยอะ และมีการแบ่ง Module เป็นจำนวนมาก จะทำให้การทำ Transitive R Classes เพิ่ม Build Time และ APK Size มากขึ้นเท่านั้น
ด้วยเหตุนี้จึงทำให้ทีมแอนดรอยด์เพิ่ม Non-transitive R Classes เข้ามาเป็นอีกทางเลือก
Non-Transitive R Classes ต่างจากเดิมอย่างไร?
รูปแบบของ Non-Transitive R Classes ก็คือแต่ละ Module จะเห็นแค่เฉพาะ Android Resource ของตัวเอง จึงทำให้ AAPT ไม่ต้องเสียเวลารวม Android Resource ของหลาย Module เข้าด้วยกัน
อ้างอิงจากแอป Slack ที่เปลี่ยนมาใช้ Non-Transitive R Classes แทน พบว่า APK Size ลดลง ~8.5% และ Build Time ลดลงจากเดิมมากถึง 14%
แต่นั่นก็หมายความว่านักพัฒนาจะเรียก Android Resource ของ Dependency ผ่าน R Class ของ Module นั้น ๆ ไม่ได้เช่นกัน
// app module
import com.sfl.R
R.string.app_name
R.string.home_menu // Unresolved reference
R.string.confirm // Unresolved reference
// feature module
import com.sfl.feature.R
R.string.home_menu
R.string.confirm // Unresolved reference
// base module
import com.sfl.base.R
R.string.confirm
จึงทำให้การใช้ Non-Transitive R Classes กับโปรเจคที่มีอยู่อาจจะเกิดปัญหาได้ทันที เพราะเป็นเรื่องปกติที่นักพัฒนาจะมี Module กลางที่เอาไว้รวม Android Resource บางอย่างเพื่อให้เรียกใช้งานใน Module อื่น ๆ ได้ง่าย
ดังนั้นเพื่อให้โปรเจคของเราใช้ Non-Transitive R Classes ได้ นักพัฒนาจะต้องแก้ไขโค้ดสำหรับ R Class เพื่อให้รองรับด้วย
การเปิดใช้งาน Non-Transitive R Classes
นักพัฒนาสามารถเปิดหรือปิดใช้งาน Non-Transitive R Classes ได้ด้วยการเพิ่ม Property เข้าไปใน gradle.properties
แบบนี้
// gradle.properties
android.nonTransitiveRClass=true
จะเปิดใช้งานก็ให้ใส่ true
แต่ถ้าอยากปิดก็ให้ใส่ false
และเมื่อสร้างโปรเจคใหม่บน Android Studio เวอร์ชันล่าสุดก็จะเปิดใช้งาน Non-Transitive R Classes ให้ตั้งแต่แรก หรือถ้าใช้ Android Gradle Plugin เวอร์ชัน 8.0 ขึ้นไปและไม่ได้กำหนด Properties ดังกล่าวก็จะถือว่าเป็นการเปิดใช้งานโดยอัตโนมัติ
แก้ไขโค้ดให้รองรับ Non-Transitive R Classes
เนื่องจากการเปิดใช้งาน Non-Transitive R Classes จะทำให้นักพัฒนาเรียก Android Resource ผ่าน R Class ของ Module นั้นได้โดยตรง จึงต้องเปลี่ยนไปเรียกผ่าน R Class ของ Module ที่ Android Resource นั้น ๆ อยู่แทน
ถ้าอ้างอิงจากตัวอย่างก่อนหน้า ถ้า :app
จะเรียก String Resource ใน :feature
หรือ :base
ก็จะเป็นแบบนี้แทน
// app module
import com.sfl.R
R.string.app_name
com.sfl.feature.R.string.home_menu
com.sfl.base.R.string.confirm
เนื่องจาก app_name
เป็น String Resource ที่อยู่ใน :app
จึงเรียกผ่าน R Class ของตัวเองได้เลย แต่ String Resource ตัวอื่นต้องเรียกจาก R Class ของ Module เหล่านั้นแทน
และบน Android Studio ก็มีตัวช่วยสำหรับแปลง Android Resource ในโค้ดให้รองรับ Non-Transitive R Classes ด้วยเช่นกัน โดยให้เปิดไฟล์ที่ต้องการแล้วเลือกเมนู Refactor > Migrate to Non-Transitive R Classes...
แต่วิธีดังกล่าวก็อาจจะไม่ค่อยถูกใจนักพัฒนาซักเท่าไร เนื่องจากโค้ดเหล่านั้นจะกลายเป็นโค้ดที่ยาวมากขึ้นเพราะใส่ Package Name ของ R Class เข้าไปตรง ๆ ดังนั้นจึงแนะนำให้ Import R Class เหล่านั้นแล้วกำหนดเป็นชื่ออื่นแทนดีกว่า
// app module
import com.sfl.R
import com.sfl.feature.R as FeatureR
import com.sfl.base.R as BaseR
R.string.app_name
FeatureR.string.home_menu
BaseR.string.confirm
วิธีนี้จะเหมาะกับโปรเจคที่มีการแยก Module สำหรับ Android Resource บางอย่างโดยเฉพาะ เช่น Drawable Resource ที่ใช้งานร่วมกันในหลาย Module หรือ String Resource สำหรับข้อความภายในแอปทั้งหมด จะได้ไม่ต้อง Import R Class ให้เยอะจนเกินไป
สรุป
Non-Transitive R Classes เป็นวิธีจัดการกับ R Class สำหรับ Android Resource แบบใหม่ที่จะช่วยลด Build Time และ APK Size ให้น้อยลง โดยลดการทำงานของ AAPT ที่เดิมทีจะต้องคอยรวม Android Resource ให้กับแต่ละ Module อยู่ตลอดเวลา
ถึงแม้ว่าการเปิดใช้งาน Non-Transitive R Classes จะต้องแลกด้วยการเปลี่ยนวิธีการเขียนโค้ดเพื่อเรียกใช้งาน Android Resource ด้วยเช่นกัน แต่ถ้าโปรเจคมีการจัดการกับ Android Resource ที่ดีและไม่ซับซ้อน ก็จะเปิดใช้งาน Non-Transitive R Classes โดยที่ไม่ต้องแก้ไขโค้ดมากนัก
ผู้ที่หลงเข้ามาอ่านคนไหนทำเสร็จเรียบร้อยแล้วก็อย่าลืมเปรียบเทียบ Build Time กับ APK Size ในระหว่างก่อนทำและหลังทำด้วยนะครับ 😉