รู้จักกับ Non-Transitive R Classes บน Android Gradle Plugin

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 ในระหว่างก่อนทำและหลังทำด้วยนะครับ 😉

แหล่งข้อมูลอ้างอิง