แก้ปัญหาความเสถียร

เมื่อพบคลาสที่ไม่เสถียรซึ่งทำให้เกิดปัญหาด้านประสิทธิภาพ คุณควรเพิ่มประสิทธิภาพวิธีที่ Compose เปรียบเทียบหรือสร้างอินสแตนซ์คลาสดังกล่าว เอกสารนี้สรุปเทคนิคหลายอย่างที่คุณใช้เพื่อดำเนินการดังกล่าวได้

เปิดใช้การข้ามแบบเข้มงวด

โหมดการข้ามที่เข้มงวดจะเปิดใช้โดยค่าเริ่มต้นตั้งแต่ Kotlin 2.0.20 เป็นต้นไป โหมดการข้ามที่เข้มงวดช่วยให้ข้าม Composable ที่มีพารามิเตอร์ไม่เสถียรได้โดยการเปรียบเทียบพารามิเตอร์ไม่เสถียรโดยใช้ความเท่ากันของอินสแตนซ์ (===) ขณะที่พารามิเตอร์ที่เสถียรจะยังคงเปรียบเทียบโดยใช้ความเท่ากันของโครงสร้าง (Object.equals())

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

ดูข้อมูลเพิ่มเติมได้ที่การข้ามที่มีประสิทธิภาพ

ทำให้คลาสไม่เปลี่ยนรูป

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

  • Immutable: ระบุประเภทที่ค่าของพร็อพเพอร์ตี้ใดๆ ไม่สามารถเปลี่ยนแปลงได้หลังจากสร้างอินสแตนซ์ของประเภทนั้น และเมธอดทั้งหมดมีความโปร่งใสในการอ้างอิง
    • ตรวจสอบว่าพร็อพเพอร์ตี้ทั้งหมดของคลาสเป็นทั้ง val แทนที่จะเป็น var และเป็นประเภทที่เปลี่ยนแปลงไม่ได้
    • ประเภทพื้นฐาน เช่น String, Int และ Float จะเปลี่ยนแปลงไม่ได้เสมอ
    • ประเภท data class แบบแบนที่มีพร็อพเพอร์ตี้ val แบบพื้นฐานหรือ String ในโมดูลที่สร้างด้วยคอมไพเลอร์ Compose จะได้รับการอนุมานว่าเสถียรและเปรียบเทียบกับ .equals() โดยอัตโนมัติอยู่แล้ว คุณจึงไม่จำเป็นต้องใส่คำอธิบายประกอบด้วย @Immutable
    • หากไม่สามารถใช้ความสามารถในการเปลี่ยนแปลงไม่ได้ คุณต้องใช้สถานะ Compose สำหรับพร็อพเพอร์ตี้ที่เปลี่ยนแปลงได้
  • Stable: บ่งบอกถึงประเภทที่เปลี่ยนแปลงได้ รันไทม์ของ Compose จะไม่ทราบว่าพร็อพเพอร์ตี้สาธารณะหรือลักษณะการทำงานของเมธอดของประเภทนั้นๆ จะให้ผลลัพธ์ที่แตกต่างจากการเรียกใช้ก่อนหน้าหรือไม่และเมื่อใด

คอลเล็กชันและความเสถียร

สาเหตุที่พบบ่อยที่ทำให้ Compose พิจารณาว่าคลาสไม่เสถียรคือคอลเล็กชัน ตามที่ระบุไว้ในหน้าวิเคราะห์ปัญหาด้านความเสถียร คอมไพเลอร์ Compose ไม่สามารถแน่ใจได้อย่างสมบูรณ์ว่าอินเทอร์เฟซคอลเล็กชัน เช่น List, Map และ Set เป็นแบบไม่เปลี่ยนแปลงอย่างแท้จริง จึงทำเครื่องหมายว่าไม่เสถียร

พิจารณาคลาสนี้จากคู่มือวินิจฉัยปัญหาด้านความเสถียร

unstable class Snack {
  …
  unstable val tags: Set<String>
  …
}

เมื่อเปิดใช้โหมดการข้ามที่เข้มงวด (ค่าเริ่มต้นใน Kotlin 2.0.20 ขึ้นไป) คอลเล็กชันและคลาสที่ไม่เสถียรซึ่งมีคอลเล็กชันจะข้ามการคอมโพสใหม่โดยใช้ความเท่ากันของอินสแตนซ์ (===) เมื่อใดก็ตามที่มีการส่งผ่านการอ้างอิงเดียวกัน

โปรดระมัดระวังก่อนแทนที่ kotlin.collections.List, Set หรือ Map ด้วย Kotlinx Immutable Collections (ImmutableList, ImmutableSet, ImmutableMap) หรือเพิ่มกฎ @Immutable, @Stable หรือ stability_config.conf ลงในโมเดลที่เก็บคอลเล็กชัน ในโหมดการข้ามที่เข้มงวด List มาตรฐานจะข้ามโดยใช้ความเท่ากันของการอ้างอิง (===) ในขณะที่ ImmutableList และโมเดลคอลเล็กชันที่มีคำอธิบายประกอบจะทำการเปรียบเทียบ .equals() ทีละองค์ประกอบในทุกๆ การส่งผ่านการจัดองค์ประกอบใหม่

สำหรับคอลเล็กชันขนาดใหญ่หรือเลย์เอาต์แบบ Lazy (LazyColumn, LazyRow, LazyGrid) การตรวจสอบ .equals() ในทุกรายการอาจใช้เวลานานกว่าการจัดองค์ประกอบใหม่ ต้นทุนนี้จะเพิ่มขึ้นเป็น 2 เท่าหากคุณส่งพร็อพเพอร์ตี้ ImmutableList จาก data class ไปยัง Composable ย่อย

data class SnackCategory(
    val name: String,
    val snacks: ImmutableList<Snack>
)

@Composable
fun CategorySection(category: SnackCategory) {
    Text(category.name)
    // Compose compares category.snacks twice: once in SnackCategory.equals()
    // for CategorySection, and again for HighlightedSnacks.
    HighlightedSnacks(snacks = category.snacks)
}

แทนที่จะบังคับให้คอลเล็กชันมีความเท่าเทียมกันเชิงโครงสร้าง ให้เก็บรักษาการอ้างอิงอินสแตนซ์ของคอลเล็กชันไว้ตลอดการปล่อย (เช่น โดยการแคชการเปลี่ยนรูปแบบใน ViewModel หรือด้วย remember) เพื่อให้ Compose ข้ามการใช้ความเท่าเทียมกันของอินสแตนซ์ (===) ได้ พิจารณาใช้ ImmutableList หรือคำอธิบายประกอบความเสถียรในโมเดลที่เก็บคอลเล็กชันเฉพาะเมื่อคุณไม่สามารถเก็บรักษาการอ้างอิงอินสแตนซ์ของคอลเล็กชันไว้ได้ และการเปรียบเทียบยืนยันว่าการตรวจสอบความเท่าเทียมกันเชิงโครงสร้างเร็วกว่าการจัดองค์ประกอบใหม่ หากคุณใช้ Kotlin เวอร์ชันต่ำกว่า 2.0.20 ให้เปิดใช้โหมดข้ามที่เข้มงวดก่อน

ใส่คำอธิบายประกอบด้วย Stable หรือ Immutable

วิธีหนึ่งที่เป็นไปได้ในการแก้ไขปัญหาความเสถียรสำหรับคลาสขนาดเล็กที่มีการตรวจสอบ .equals() ที่ไม่ซับซ้อนคือการใส่คำอธิบายประกอบคลาสที่ไม่เสถียรด้วย @Stable หรือ @Immutable

การใส่คำอธิบายประกอบคลาสจะลบล้างสิ่งที่คอมไพเลอร์จะอนุมานเกี่ยวกับคลาสของคุณ ซึ่งคล้ายกับโอเปอเรเตอร์ !! ใน Kotlin คุณควรระมัดระวังอย่างยิ่งเกี่ยวกับวิธีใช้คำอธิบายประกอบเหล่านี้ การลบล้างลักษณะการทำงานของคอมไพเลอร์อาจทำให้เกิดข้อบกพร่องที่ไม่คาดคิด เช่น Composable ไม่จัดองค์ประกอบใหม่ตามที่คุณคาดหวัง

เมื่อเปิดใช้โหมดข้ามแบบเข้มงวด ให้ทำตามหลักเกณฑ์ต่อไปนี้เมื่อตัดสินใจว่าจะเพิ่ม @Stable หรือ @Immutable

  • อย่าใส่คำอธิบายประกอบคลาสที่อนุมานว่าเสถียรแล้ว: คลาสที่มีพร็อพเพอร์ตี้ val ของประเภทที่เสถียรในโมดูลที่สร้างด้วยคอมไพเลอร์ Compose จะอนุมานว่าเสถียรแล้ว
  • ใส่คำอธิบายประกอบเฉพาะคลาสที่มีการตรวจสอบ .equals() ที่ใช้ทรัพยากรน้อยเมื่อมีการสร้างอินสแตนซ์ใหม่: เพิ่ม @Stable หรือ @Immutable เมื่อคอมไพเลอร์ไม่สามารถอนุมานความเสถียรได้ (เช่น อินเทอร์เฟซหรือคลาสจากโมดูลที่ไม่ใช่ Compose) และแหล่งข้อมูลจะจัดสรรอินสแตนซ์ออบเจ็กต์ใหม่สำหรับรายการที่ไม่มีการเปลี่ยนแปลงในการปล่อยแต่ละครั้ง โดยมีเงื่อนไขว่าคลาสนั้นมีขนาดเล็กและมีการตรวจสอบ .equals() ที่ใช้ทรัพยากรน้อย
  • ระมัดระวังโมเดลที่มีคอลเล็กชัน: หลีกเลี่ยงการเพิ่ม @Stable หรือ @Immutable ลงในโมเดลข้อมูลที่มีคอลเล็กชันขนาดใหญ่ (List, Set, Map) โดยเฉพาะโมเดลที่ใช้ในเลย์เอาต์แบบ Lazy (LazyColumn, LazyRow, LazyGrid, Pager) การปล่อยโมเดลเหล่านั้นไว้โดยไม่มีคำอธิบายประกอบจะช่วยให้ Compose เปรียบเทียบโมเดลเหล่านั้นโดยใช้ความเท่ากันของอินสแตนซ์ (===) แทนการตรวจสอบโครงสร้าง .equals() ในทุกองค์ประกอบ

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

@Immutable
data class SnackSummary(
    val id: Long,
    val name: String,
    val price: Long
)

ไม่ว่าคุณจะใช้คำอธิบายประกอบ @Immutable หรือ @Stable คอมไพเลอร์ Compose จะทำเครื่องหมายคลาส SnackSummary ว่าเสถียรและเปรียบเทียบโดยใช้ Object.equals()

คลาสที่มีคำอธิบายประกอบในคอลเล็กชัน

พิจารณา Composable ที่มีพารามิเตอร์ประเภท List<Snack> ดังนี้

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  unstable snacks: List<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

แม้ว่า Snack จะเสถียรหรือมีคำอธิบายประกอบเป็น @Immutable แต่คอมไพเลอร์ Compose ก็ยังคงทำเครื่องหมายพารามิเตอร์ snacks ใน HighlightedSnacks เป็น unstable เนื่องจาก List เป็นอินเทอร์เฟซ

เมื่อเปิดใช้โหมดข้ามที่เข้มงวด ระบบจะยังคงทำเครื่องหมาย HighlightedSnacks เป็นskippable และข้ามการจัดองค์ประกอบใหม่โดยใช้ความเท่าเทียมกันของอินสแตนซ์ (===) เมื่อมีการส่งอินสแตนซ์ List เดียวกัน คุณสามารถทำให้พารามิเตอร์ snacks เป็น stable ได้โดยการรวม List<Snack> ไว้ในคลาส Wrapper @Immutable, แทนที่ List<Snack> ด้วย ImmutableList<Snack> หรือเพิ่ม kotlin.collections.* ลงในไฟล์การกำหนดค่าความเสถียร

อย่างไรก็ตาม การทำเช่นนี้จะทำให้ Compose ทำการเปรียบเทียบ.equals() ทีละองค์ประกอบในทุกๆ การส่งผ่านการจัดองค์ประกอบใหม่ ใช้วิธีการเหล่านี้เฉพาะเมื่อคุณไม่สามารถเก็บรักษาการอ้างอิงอินสแตนซ์ของรายการไว้ได้ และการเปรียบเทียบแสดงให้เห็นถึงประโยชน์

ไฟล์การกำหนดค่าความเสถียร

ตั้งแต่ Compose Compiler 1.5.5 เป็นต้นไป คุณสามารถระบุไฟล์การกำหนดค่าของคลาสที่ถือว่าเสถียรได้ในเวลาคอมไพล์ ซึ่งช่วยให้พิจารณาคลาสที่คุณไม่ได้ควบคุม เช่น คลาสไลบรารีมาตรฐานอย่าง LocalDateTime ว่ามีความเสถียรได้

ไฟล์การกำหนดค่าเป็นไฟล์ข้อความธรรมดาที่มี 1 คลาสต่อแถว ระบบรองรับความคิดเห็น ไวลด์การ์ดเดี่ยว และไวลด์การ์ดคู่

ตัวอย่างการกำหนดค่า

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

หากต้องการเปิดใช้ฟีเจอร์นี้ ให้ส่งเส้นทางของไฟล์การกำหนดค่าไปยังบล็อกตัวเลือก composeCompiler ของการกำหนดค่าปลั๊กอิน Gradle ของคอมไพเลอร์ Compose

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

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

หลายโมดูล

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

หากชั้นข้อมูลอยู่ในโมดูลแยกต่างหากจากชั้น UI ซึ่งเป็นแนวทางที่แนะนำ คุณอาจพบปัญหานี้ ในโหมดการข้ามที่เข้มงวด ระบบจะถือว่าคลาสจากโมดูลที่ไม่ใช่ Compose ไม่เสถียร และยังคงข้ามการจัดองค์ประกอบใหม่โดยใช้ความเท่าเทียมกันของอินสแตนซ์ (===) เมื่อมีการส่งอินสแตนซ์เดียวกัน อย่างไรก็ตาม หากชั้นข้อมูลของคุณจัดสรรอินสแตนซ์ใหม่ของโมเดลขนาดเล็กแบบแบนที่มีค่าเหมือนกันในการปล่อยแต่ละครั้งบ่อยครั้ง คุณสามารถเลือกใช้ความเท่าเทียมกันเชิงโครงสร้าง (Object.equals()) ได้

โซลูชัน

หากต้องการแก้ไขปัญหานี้ คุณสามารถใช้วิธีใดวิธีหนึ่งต่อไปนี้

  1. เพิ่มคลาสขนาดเล็กแบบแบนที่มีการตรวจสอบ .equals() ที่ใช้ทรัพยากรน้อยลงในไฟล์การกำหนดค่าคอมไพเลอร์
  2. เปิดใช้คอมไพเลอร์ Compose ในโมดูลชั้นข้อมูล หรือติดแท็กคลาสด้วย @Stable หรือ @Immutable ตามความเหมาะสม
    • ซึ่งเกี่ยวข้องกับการเพิ่มทรัพยากร Dependency ของ Compose ลงในชั้นข้อมูล อย่างไรก็ตาม นี่เป็นเพียงทรัพยากร Dependency สำหรับรันไทม์ Compose ไม่ใช่สำหรับ Compose-UI
  3. ภายในโมดูล UI ให้รวมคลาสชั้นข้อมูลไว้ในคลาส Wrapper ที่เจาะจง UI

ปัญหานี้ยังเกิดขึ้นเมื่อใช้ไลบรารีภายนอกหากไม่ได้ใช้คอมไพเลอร์ Compose

ไม่ใช่ว่า Composable ทุกรายการจะข้ามได้

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

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

  • Composable ที่ไม่ได้จัดองค์ประกอบใหม่บ่อยๆ หรือไม่จัดองค์ประกอบใหม่เลย
  • Composable ที่เรียกใช้ Composable ที่ข้ามได้เท่านั้น
  • คอมโพสิเบิลที่มีพารามิเตอร์จำนวนมากหรือโมเดลคอลเล็กชันที่มีการใช้งาน .equals() ที่มีค่าใช้จ่ายสูง ในกรณีนี้ ต้นทุนในการตรวจสอบว่าพารามิเตอร์มีการเปลี่ยนแปลงหรือไม่ อาจมากกว่าต้นทุนของการจัดองค์ประกอบใหม่ที่มีต้นทุนต่ำ

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