คุณอาจพบข้อผิดพลาดที่พบบ่อยเกี่ยวกับ 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
แหล่งข้อมูลเพิ่มเติม
- คู่มือประสิทธิภาพของแอป: ดูแนวทางปฏิบัติแนะนำ ไลบรารี และเครื่องมือต่างๆ เพื่อปรับปรุงประสิทธิภาพใน Android
- ตรวจสอบประสิทธิภาพ: ตรวจสอบประสิทธิภาพของแอป
- การเปรียบเทียบ: เปรียบเทียบประสิทธิภาพของแอป
- การเริ่มต้นแอป: เพิ่มประสิทธิภาพการเริ่มต้นแอป
- โปรไฟล์พื้นฐาน: ทำความเข้าใจโปรไฟล์พื้นฐาน
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- สถานะและ Jetpack Compose
- ตัวแก้ไขกราฟิก
- การคิดใน Compose