ทำตามแนวทางปฏิบัติแนะนำ

คุณอาจพบข้อผิดพลาดที่พบบ่อยเกี่ยวกับ Compose ข้อผิดพลาดเหล่านี้อาจทำให้คุณได้โค้ดที่ดูเหมือนจะทำงานได้ดีพอ แต่ก็อาจส่งผลเสียต่อประสิทธิภาพของ UI ทำตามแนวทางปฏิบัติแนะนำเพื่อเพิ่มประสิทธิภาพแอปใน Compose

ใช้ remember เพื่อลดการคำนวณที่มีค่าใช้จ่ายสูง

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

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

ตัวอย่างเช่น โค้ดต่อไปนี้แสดงรายการชื่อที่จัดเรียงแล้ว แต่ใช้วิธีการจัดเรียงที่ใช้ทรัพยากรมาก

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

ทุกครั้งที่มีการจัดองค์ประกอบ ContactsList ใหม่ ระบบจะจัดเรียงข้อมูลรายชื่อติดต่อทั้งหมดอีกครั้ง แม้ว่าข้อมูลรายชื่อติดต่อจะไม่มีการเปลี่ยนแปลงก็ตาม หากผู้ใช้เลื่อนรายการ Composable จะได้รับการจัดองค์ประกอบใหม่ทุกครั้งที่แถวใหม่ปรากฏขึ้น

วิธีแก้ปัญหานี้คือจัดเรียงรายการนอก LazyColumn และจัดเก็บรายการที่จัดเรียงแล้วด้วย remember ดังนี้

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

ตอนนี้ระบบจะจัดเรียงรายการเพียงครั้งเดียวเมื่อสร้าง ContactList เป็นครั้งแรก หากรายชื่อติดต่อหรือตัวเปรียบเทียบมีการเปลี่ยนแปลง ระบบจะสร้างรายการที่จัดเรียงใหม่ ไม่เช่นนั้น Composable จะใช้รายการที่จัดเรียงที่แคชไว้ต่อไปได้

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

โปรดระมัดระวังเมื่อส่งคอลเล็กชันขนาดใหญ่หรือออบเจ็กต์ที่มีการใช้งาน equals() ที่มีค่าใช้จ่ายสูงเป็นคีย์ไปยัง remember สำหรับการคำนวณที่มีค่าใช้จ่ายต่ำ เนื่องจากการเปรียบเทียบคีย์อาจมีค่าใช้จ่ายมากกว่าการเรียกใช้การคำนวณโดยตรง

@Composable
fun ContactHeader(contacts: List<Contact>) {
    // Avoid: Comparing the `contacts` list with .equals() on recomposition
    // is more expensive than reading contacts.size or contacts.firstOrNull() directly.
    val firstContactName = remember(contacts) {
        contacts.firstOrNull()?.name.orEmpty()
    }
    Text(firstContactName)
}

ใช้คีย์เลย์เอาต์แบบ Lazy และประเภทเนื้อหา

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

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

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

แต่โค้ดนี้มีปัญหา สมมติว่ามีการเปลี่ยนโน้ตด้านล่าง ตอนนี้โน้ตดังกล่าวเป็นโน้ตที่แก้ไขล่าสุด จึงย้ายไปอยู่ที่ด้านบนสุดของรายการ และโน้ตอื่นๆ ทั้งหมดจะเลื่อนลงไป 1 อันดับ

หากไม่มีความช่วยเหลือจากคุณ Compose จะไม่ทราบว่ารายการที่ไม่มีการเปลี่ยนแปลงเป็นเพียงรายการที่ย้ายในลิสต์ แต่ Compose จะคิดว่า "รายการที่ 2" เดิมถูกลบไปแล้ว และมีการสร้างรายการใหม่สำหรับรายการที่ 3, รายการที่ 4 และรายการต่อๆ ไปทั้งหมด ผลลัพธ์ที่ได้คือ Compose จะจัดองค์ประกอบทุกรายการในลิสต์ใหม่ แม้ว่าจะมีเพียงรายการเดียวที่เปลี่ยนแปลงจริงๆ

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

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

เมื่อทำงานกับเลย์เอาต์แบบ Lazy (LazyColumn, LazyRow, LazyGrid) โปรดคำนึงถึงหลักเกณฑ์ต่อไปนี้ด้วย

  • ใช้คีย์ที่ไม่ซ้ำกันซึ่งบันทึกได้ใน Bundle: คีย์ต้องไม่ซ้ำกัน เป็นแบบเชิงกำหนด และบันทึกได้ใน Bundle ของ Android เนื่องจากเลย์เอาต์แบบ Lazy ใช้ rememberSaveable ภายใน แนะนำให้ใช้ประเภทพื้นฐาน เช่น Int, Long หรือ String และอย่าใช้ดัชนีรายการหรือ hashCode() เป็นคีย์
  • ระบุ contentType สำหรับรายการที่ต่างกันเสมอ: ส่ง contentType = { it.type } ไปยัง items หรือ itemsIndexed เพื่อให้ Compose นำช่อง Composition มาใช้ซ้ำระหว่างรายการที่ใช้โครงสร้างภาพเดียวกันในระหว่างการเลื่อนเท่านั้น
  • ใช้ itemsIndexed แทน indexOf: หลีกเลี่ยงการเรียกใช้ list.indexOf(item) ภายในบล็อก items ซึ่งจะเปลี่ยนเลย์เอาต์เป็นการสแกน O(N²) ให้ใช้ itemsIndexed แทน
  • ยกสถานะที่แชร์และการอ่าน CompositionLocal: รวบรวมอินสแตนซ์ Flow ที่แชร์และอ่านค่า CompositionLocal (เช่น LocalContext.current หรือ LocalDensity.current) 1 ครั้งใน Composable ระดับบนเหนือเลย์เอาต์แบบ Lazy แล้วส่งค่าที่แก้ไขแล้วไปยัง Composable ของรายการ
  • ใช้ Row หรือ Column สำหรับรายการขนาดเล็กที่มีขอบเขต: สำหรับคอลเล็กชันแบบคงที่หรือมีขอบเขตขนาดเล็ก (เช่น 1 ถึง 10 รายการ) การใช้ Row หรือ Column มาตรฐานที่มี forEach จะช่วยหลีกเลี่ยงการใช้ทรัพยากรมากเกินไปในการทำเวอร์ชวลไลเซชันแบบ Lazy และการดึงข้อมูลล่วงหน้า

ใช้ computedStateOf หรือ derivedStateOf เพื่อจำกัดการจัดองค์ประกอบใหม่

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

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

ปัญหาคือ หากผู้ใช้เลื่อนรายการ listState จะเปลี่ยนแปลงตลอดเวลาเมื่อผู้ใช้ลากนิ้ว ซึ่งหมายความว่ารายการจะมีการจัดเรียงใหม่ตลอดเวลา อย่างไรก็ตาม คุณไม่จำเป็นต้องจัดองค์ประกอบใหม่บ่อยขนาดนั้น คุณไม่จำเป็นต้องจัดองค์ประกอบใหม่จนกว่ารายการใหม่จะปรากฏที่ด้านบน ซึ่งทำให้ต้องมีการคำนวณเพิ่มเติมจำนวนมาก และทำให้ UI ทำงานได้ไม่ดี

วิธีแก้ปัญหาคือใช้ derived state (หรือ computedStateOf ใน Compose Runtime 1.13 ขึ้นไป) สถานะที่ได้มาช่วยให้คุณบอก Compose ได้ว่าการเปลี่ยนแปลงสถานะใดที่ควรทริกเกอร์การจัดองค์ประกอบใหม่จริงๆ ในกรณีนี้ ให้ระบุว่าคุณสนใจเมื่อรายการแรกที่มองเห็นได้มีการเปลี่ยนแปลง เมื่อค่าสถานะนั้นเปลี่ยนแปลง UI จะต้องจัดองค์ประกอบใหม่ แต่หากผู้ใช้ยังเลื่อนไม่มากพอที่จะนำรายการใหม่มาไว้ด้านบน ก็ไม่จำเป็นต้องจัดองค์ประกอบใหม่

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

เลือกแนวทางการรับสถานะที่เหมาะสมตามเวอร์ชัน Compose และต้นทุนการคำนวณ ดังนี้

  • remember { computedStateOf { ... } } (Compose Runtime 1.13 ขึ้นไป): ประเมินการคำนวณอีกครั้งทุกครั้งที่อ่านโดยไม่แคชผลลัพธ์ และจะทำให้ Composition ไม่ถูกต้องก็ต่อเมื่อเอาต์พุตที่คำนวณมีการเปลี่ยนแปลงเท่านั้น เนื่องจากช่วยหลีกเลี่ยงการทำบัญชีแคชและค่าใช้จ่ายเพิ่มเติมในการเขียนกลับการทำให้เป็นโมฆะ จึงควรใช้ remember { computedStateOf { listState.firstVisibleItemIndex > 0 } } สำหรับการตรวจสอบเงื่อนไขพื้นฐานและการดึงข้อมูลแบบไม่ซับซ้อนซึ่งขับเคลื่อนโดยสถานะที่เปลี่ยนแปลงอย่างรวดเร็ว (เช่น ออฟเซ็ตการเลื่อน)
  • remember { derivedStateOf { ... } }: แคชผลลัพธ์ของการคำนวณในการอ่านหลายครั้ง ใช้ derivedStateOf ใน Compose เวอร์ชันที่ต่ำกว่า 1.13 หรือเมื่อการคำนวณใช้ทรัพยากรในการประมวลผลสูงและอ่านหลายครั้งในระหว่างการส่งผ่าน Composition อย่าส่งค่าสถานะสแนปชอตที่อ่านภายใน derivedStateOf (เช่น listState.firstVisibleItemIndex) เป็นคีย์ remember เนื่องจากจะทำให้ระบบสร้าง DerivedState ใหม่ทุกครั้งที่มีการเปลี่ยนแปลงสถานะและป้องกันการข้าม
  • เมธอด Getter ของ Kotlin แบบธรรมดา (get()) และการอ่านพร็อพเพอร์ตี้โดยตรง: แนะนำให้ใช้เมธอด Getter ที่กำหนดเองแบบธรรมดาสำหรับนิพจน์บูลีนทั่วไปที่ไม่จำเป็นต้องลดการลบล้าง และอ่านคอลเล็กชัน .size โดยตรงแทนที่จะรวมไว้ใน derivedStateOf หลีกเลี่ยงการใช้ produceState กับ snapshotFlow สำหรับการรับสถานะแบบซิงโครนัส เนื่องจากจะเพิ่มค่าใช้จ่ายในการจัดส่ง Coroutine ที่ไม่จำเป็น

เลื่อนการอ่านให้นานที่สุด

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

เพื่อให้ได้ผลลัพธ์นี้ คอมโพสิเบิล Title ต้องมีออฟเซ็ตการเลื่อนเพื่อออฟเซ็ตตัวเองโดยใช้ Modifier นี่คือโค้ด Jetsnack เวอร์ชันที่เรียบง่ายขึ้นก่อนที่จะทำการเพิ่มประสิทธิภาพ

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

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

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

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

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

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

ก่อนหน้านี้ โค้ดใช้ Modifier.offset(x: Dp, y: Dp) ซึ่งใช้ค่าออฟเซ็ตเป็นพารามิเตอร์ การเปลี่ยนไปใช้ตัวแก้ไขเวอร์ชัน Lambda จะช่วยให้มั่นใจได้ว่าฟังก์ชันจะอ่านสถานะการเลื่อนในระยะเลย์เอาต์ ด้วยเหตุนี้ เมื่อสถานะการเลื่อนเปลี่ยนไป Compose จึงข้ามเฟสการจัดองค์ประกอบไปเลยได้ และไปยังเฟสเลย์เอาต์โดยตรง เมื่อส่งตัวแปรสถานะที่เปลี่ยนแปลงบ่อยไปยังตัวแก้ไข คุณควรใช้ตัวแก้ไขเวอร์ชัน Lambda ทุกครั้งที่เป็นไปได้

ตัวอย่างอีกรายการของแนวทางนี้มีดังนี้ โค้ดนี้ยังไม่ได้รับการเพิ่มประสิทธิภาพ

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

ในตัวอย่างนี้ สีพื้นหลังของกล่องจะสลับไปมาระหว่าง 2 สีอย่างรวดเร็ว สถานะนี้จึงเปลี่ยนแปลงบ่อยมาก จากนั้น Composable จะอ่านสถานะนี้ในตัวปรับแต่งพื้นหลัง ด้วยเหตุนี้ กล่องจึงต้องจัดองค์ประกอบใหม่ในทุกเฟรม เนื่องจากสีมีการเปลี่ยนแปลงในทุกเฟรม

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

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

หลีกเลี่ยงการเขียนย้อนกลับ

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

Composable ต่อไปนี้แสดงตัวอย่างข้อผิดพลาดประเภทนี้

@Composable
fun BadComposable() {
    var count by remember { mutableIntStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

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

คุณหลีกเลี่ยงการเขียนย้อนกลับได้โดยไม่เขียนไปยังสถานะใน Composition หากเป็นไปได้ ให้เขียนไปยังสถานะเพื่อตอบสนองต่อเหตุการณ์และใน Lambda เสมอ เช่นในตัวอย่าง onClick ก่อนหน้า

ดูคำแนะนำโดยละเอียดเกี่ยวกับการเขียนย้อนกลับ ลูปการจัดองค์ประกอบใหม่ และการประสานงานเฟสได้ที่การเขียนย้อนกลับใน Compose

แหล่งข้อมูลเพิ่มเติม