Jetpack Compose สร้างขึ้นโดยใช้ Kotlin ในบางกรณี Kotlin มีสำนวนพิเศษที่ช่วยให้เขียนโค้ด Compose ที่ดีได้ง่ายขึ้น หากคุณคิดเป็นภาษาโปรแกรมอื่นและแปลภาษานั้นเป็น Kotlin ในใจ คุณอาจพลาดจุดแข็งบางอย่างของ Compose และอาจพบว่าการทำความเข้าใจโค้ด Kotlin ที่เขียนตามสำนวนเป็นเรื่องยาก การทำความคุ้นเคยกับรูปแบบของ Kotlin มากขึ้นจะช่วยให้คุณหลีกเลี่ยงข้อผิดพลาดเหล่านั้นได้
อาร์กิวเมนต์เริ่มต้น
เมื่อเขียนฟังก์ชัน Kotlin คุณสามารถระบุค่าเริ่มต้นสำหรับอาร์กิวเมนต์ของฟังก์ชัน ซึ่งจะใช้หากผู้เรียกไม่ได้ส่งค่าเหล่านั้นอย่างชัดแจ้ง ฟีเจอร์นี้ช่วยลดความจำเป็นในการใช้ฟังก์ชันที่โอเวอร์โหลด
เช่น สมมติว่าคุณต้องการเขียนฟังก์ชันที่วาดสี่เหลี่ยมจัตุรัส ฟังก์ชันนั้นอาจมีพารามิเตอร์ที่จำเป็นเพียงรายการเดียวคือ sideLength ซึ่งระบุความยาวของแต่ละด้าน โดยอาจมีพารามิเตอร์ที่ไม่บังคับหลายรายการ เช่น thickness, edgeColor และอื่นๆ หากผู้เรียกไม่ได้ระบุพารามิเตอร์เหล่านั้น ฟังก์ชันจะใช้ค่าเริ่มต้น ในภาษาอื่นๆ คุณอาจต้องเขียนฟังก์ชันหลายรายการ ดังนี้
// We don't need to do this in Kotlin! void drawSquare(int sideLength) { } void drawSquare(int sideLength, int thickness) { } void drawSquare(int sideLength, int thickness, Color edgeColor) { }
ใน Kotlin คุณสามารถเขียนฟังก์ชันเดียวและระบุค่าเริ่มต้นสำหรับอาร์กิวเมนต์ได้ ดังนี้
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
นอกจากจะช่วยให้คุณไม่ต้องเขียนฟังก์ชันที่ซ้ำซ้อนหลายฟังก์ชันแล้ว ฟีเจอร์นี้ยังช่วยให้โค้ดของคุณอ่านง่ายขึ้นมากด้วย หากผู้เรียกไม่ได้ระบุค่าสำหรับอาร์กิวเมนต์ แสดงว่าผู้เรียกยินดีที่จะใช้ค่าเริ่มต้น นอกจากนี้ พารามิเตอร์ที่มีชื่อยังช่วยให้ดูสิ่งที่เกิดขึ้นได้ง่ายขึ้นมาก หากดูโค้ดและเห็นการเรียกใช้ฟังก์ชันเช่นนี้ คุณอาจไม่ทราบความหมายของพารามิเตอร์โดยไม่ตรวจสอบโค้ด drawSquare()
drawSquare(30, 5, Color.Red);
ในทางตรงกันข้าม โค้ดนี้จะอธิบายตัวเองได้
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
ไลบรารี Compose ส่วนใหญ่ใช้อาร์กิวเมนต์เริ่มต้น และแนวทางปฏิบัติแนะนำคือให้ทำเช่นเดียวกันกับฟังก์ชันที่ประกอบกันได้ที่คุณเขียน แนวทางปฏิบัตินี้ช่วยให้คุณปรับแต่ง Composable ได้ แต่ยังคงทำให้ลักษณะการทำงานเริ่มต้นเรียกใช้ได้ง่าย ตัวอย่างเช่น คุณอาจสร้างองค์ประกอบของข้อความง่ายๆ ดังนี้
Text(text = "Hello, Android!")
โค้ดดังกล่าวมีผลเหมือนกับโค้ดต่อไปนี้ซึ่งมีรายละเอียดมากกว่า โดยมีการตั้งค่าพารามิเตอร์ Text อย่างชัดเจนมากขึ้น
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
ข้อมูลโค้ดแรกไม่เพียงแต่จะง่ายกว่าและอ่านง่ายกว่ามากเท่านั้น แต่ยังเป็นเอกสารที่อธิบายตัวเองด้วย การระบุเฉพาะพารามิเตอร์ text หมายความว่าคุณต้องการใช้ค่าเริ่มต้นสำหรับพารามิเตอร์อื่นๆ ทั้งหมด ในทางตรงกันข้าม ข้อมูลโค้ดที่ 2 บ่งบอกว่าคุณต้องการตั้งค่าพารามิเตอร์อื่นๆ เหล่านั้นอย่างชัดเจน แม้ว่าค่าที่คุณตั้งไว้จะเป็นค่าเริ่มต้นของฟังก์ชันก็ตาม
ฟังก์ชันลำดับสูงและนิพจน์ Lambda
Kotlin รองรับฟังก์ชันลำดับสูง ซึ่งเป็นฟังก์ชันที่รับฟังก์ชันอื่นๆ เป็นพารามิเตอร์ Compose สร้างขึ้นจากแนวทางนี้ ตัวอย่างเช่น ฟังก์ชันที่ประกอบกันได้ Button มีพารามิเตอร์ lambda onClick ค่าของพารามิเตอร์นั้นคือฟังก์ชัน ซึ่งปุ่มจะเรียกใช้เมื่อผู้ใช้คลิกปุ่ม
Button( // ... onClick = myClickFunction ) // ...
ฟังก์ชันลำดับสูงจะจับคู่กับนิพจน์ Lambda โดยอัตโนมัติ ซึ่งเป็นนิพจน์ที่ประเมินเป็นฟังก์ชัน หากต้องการใช้ฟังก์ชันเพียงครั้งเดียว คุณไม่จำเป็นต้องกำหนดฟังก์ชันไว้ที่อื่นเพื่อส่งไปยังฟังก์ชันลำดับสูง แต่คุณสามารถกำหนดฟังก์ชันได้ทันทีด้วยนิพจน์ Lambda
ตัวอย่างก่อนหน้านี้ถือว่ามีการกำหนด myClickFunction() ไว้ที่อื่น
แต่หากคุณใช้ฟังก์ชันนั้นที่นี่เท่านั้น การกำหนดฟังก์ชันในบรรทัดด้วยนิพจน์ Lambda จะง่ายกว่า ดังนี้
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
แลมบ์ดาต่อท้าย
Kotlin มีไวยากรณ์พิเศษสำหรับการเรียกใช้ฟังก์ชันลำดับสูงที่มีพารามิเตอร์สุดท้ายเป็น Lambda หากต้องการส่งนิพจน์ Lambda เป็นพารามิเตอร์ดังกล่าว คุณสามารถใช้ไวยากรณ์ Lambda ต่อท้ายได้ แทนที่จะใส่นิพจน์ Lambda ไว้ในวงเล็บ คุณจะใส่ไว้หลังวงเล็บแทน สถานการณ์นี้พบได้บ่อยใน Compose คุณจึงต้องคุ้นเคยกับลักษณะของโค้ด
ตัวอย่างเช่น พารามิเตอร์สุดท้ายของเลย์เอาต์ทั้งหมด เช่น ฟังก์ชันที่ประกอบกันได้ Column() คือ content ซึ่งเป็นฟังก์ชันที่แสดงองค์ประกอบ UI ย่อย สมมติว่าคุณต้องการสร้างคอลัมน์ที่มีองค์ประกอบของข้อความ 3 รายการ และคุณต้องใช้การจัดรูปแบบบางอย่าง โค้ดนี้จะทำงานได้ แต่ก็ยุ่งยากมาก
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
เนื่องจากพารามิเตอร์ content เป็นพารามิเตอร์สุดท้ายในซิกเนเจอร์ของฟังก์ชัน และเรากำลังส่งค่าของพารามิเตอร์เป็นนิพจน์ Lambda เราจึงสามารถดึงพารามิเตอร์ออกจากวงเล็บได้ ดังนี้
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
ทั้ง 2 ตัวอย่างมีความหมายเหมือนกันทุกประการ วงเล็บปีกกาจะกำหนดนิพจน์ Lambda ที่ส่งไปยังพารามิเตอร์ content
อันที่จริงแล้ว หากพารามิเตอร์เดียวที่คุณส่งคือ Lambda ต่อท้าย นั่นคือหากพารามิเตอร์สุดท้ายเป็น Lambda และคุณไม่ได้ส่งพารามิเตอร์อื่นๆ คุณสามารถละเว้นวงเล็บทั้งหมดได้ ตัวอย่างเช่น สมมติว่าคุณไม่จำเป็นต้องส่งตัวปรับแต่งไปยัง Column คุณอาจเขียนโค้ดดังนี้
Column { Text("Some text") Text("Some more text") Text("Last text") }
ไวยากรณ์นี้พบได้บ่อยใน Compose โดยเฉพาะสำหรับองค์ประกอบเลย์เอาต์ เช่น Column พารามิเตอร์สุดท้ายคือนิพจน์ Lambda ที่กำหนดองค์ประกอบย่อยขององค์ประกอบ และองค์ประกอบย่อยเหล่านั้นจะระบุไว้ในวงเล็บปีกกาหลังการเรียกใช้ฟังก์ชัน
ขอบเขตและตัวรับ
เมธอดและพร็อพเพอร์ตี้บางรายการใช้ได้เฉพาะในขอบเขตที่กำหนดเท่านั้น ขอบเขตที่จำกัดช่วยให้คุณนำเสนอฟังก์ชันการทำงานในจุดที่จำเป็น และหลีกเลี่ยงการใช้ฟังก์ชันการทำงานนั้นโดยไม่ตั้งใจในจุดที่ไม่เหมาะสม
ลองดูตัวอย่างที่ใช้ใน Compose เมื่อคุณเรียกใช้คอมโพสิเบิลเลย์เอาต์ Row ระบบจะเรียกใช้แลมบ์ดาเนื้อหาภายใน RowScope โดยอัตโนมัติ
ซึ่งจะช่วยให้ Row แสดงฟังก์ชันการทำงานที่ใช้ได้เฉพาะภายใน Row เท่านั้น
ตัวอย่างต่อไปนี้แสดงวิธีที่ Row แสดงค่าเฉพาะแถวสำหรับตัวแก้ไข align
Row { Text( text = "Hello world", // This Text is inside a RowScope so it has access to // Alignment.CenterVertically but not to // Alignment.CenterHorizontally, which would be available // in a ColumnScope. modifier = Modifier.align(Alignment.CenterVertically) ) }
API บางรายการยอมรับ Lambda ที่เรียกใช้ในขอบเขตผู้รับ แลมบ์ดาเหล่านั้นมีสิทธิ์เข้าถึงพร็อพเพอร์ตี้และฟังก์ชันที่กำหนดไว้ที่อื่น โดยอิงตามการประกาศพารามิเตอร์ ดังนี้
Box( modifier = Modifier.drawBehind { // This method accepts a lambda of type DrawScope.() -> Unit // therefore in this lambda we can access properties and functions // available from DrawScope, such as the `drawRectangle` function. drawRect( /*...*/ /* ... ) } )
ดูข้อมูลเพิ่มเติมได้ที่ฟังก์ชันลิเทอรัลที่มีรีซีฟเวอร์ในเอกสารประกอบของ Kotlin
พร็อพเพอร์ตี้ที่ได้รับมอบสิทธิ์
Kotlin รองรับพร็อพเพอร์ตี้ที่ได้รับมอบสิทธิ์ พร็อพเพอร์ตี้เหล่านี้จะถูกเรียกใช้เหมือนกับเป็นฟิลด์ แต่มูลค่าจะกำหนดแบบไดนามิกโดยการประเมินนิพจน์ คุณจะสังเกตเห็นพร็อพเพอร์ตี้เหล่านี้ได้จากการใช้ไวยากรณ์ by ดังนี้
class DelegatingClass { var name: String by nameGetterFunction() // ... }
โค้ดอื่นๆ จะเข้าถึงพร็อพเพอร์ตี้ได้ด้วยโค้ดดังนี้
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
เมื่อ println() ทำงาน ระบบจะเรียก nameGetterFunction() เพื่อแสดงผลค่าของสตริง
พร็อพเพอร์ตี้ที่ได้รับมอบสิทธิ์เหล่านี้มีประโยชน์อย่างยิ่งเมื่อคุณทำงานกับพร็อพเพอร์ตี้ที่ได้รับการสนับสนุนจากสถานะ
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
การแยกโครงสร้างคลาสข้อมูล
หากคุณกำหนดคลาสข้อมูล คุณจะเข้าถึงข้อมูลได้ด้วยการประกาศการแยกโครงสร้าง ตัวอย่างเช่น สมมติว่าคุณกำหนดคลาส Person ดังนี้
data class Person(val name: String, val age: Int)
หากมีออบเจ็กต์ประเภทนั้น คุณจะเข้าถึงค่าของออบเจ็กต์ได้ด้วยโค้ดดังนี้
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
คุณมักจะเห็นโค้ดประเภทนั้นในฟังก์ชัน Compose ดังนี้
Row { val (image, title, subtitle) = createRefs() // The `createRefs` function returns a data object; // the first three components are extracted into the // image, title, and subtitle variables. // ... }
คลาสข้อมูลมีฟังก์ชันการทำงานอื่นๆ ที่เป็นประโยชน์มากมาย ตัวอย่างเช่น เมื่อคุณกำหนดคลาสข้อมูล คอมไพเลอร์จะกำหนดฟังก์ชันที่เป็นประโยชน์โดยอัตโนมัติ เช่น equals() และ copy() ดูข้อมูลเพิ่มเติมได้ในเอกสารประกอบเกี่ยวกับคลาสข้อมูล
ออบเจ็กต์ Singleton
Kotlin ช่วยให้คุณประกาศ Singleton ซึ่งเป็นคลาสที่มีอินสแตนซ์เพียงรายการเดียวเสมอ ซิงเกิลตันเหล่านี้จะประกาศด้วยคีย์เวิร์ด object
Compose มักใช้ออบเจ็กต์ดังกล่าว เช่น MaterialTheme จะกำหนดเป็นออบเจ็กต์ซิงเกิลตัน โดยพร็อพเพอร์ตี้ MaterialTheme.colors, shapes และ typography ทั้งหมดจะมีค่าสำหรับธีมปัจจุบัน
เครื่องมือสร้างที่ปลอดภัยในการกำหนดประเภทและ DSL
Kotlin ช่วยให้สร้างภาษาเฉพาะโดเมน (DSL) ด้วยเครื่องมือสร้างที่ปลอดภัยในการกำหนดประเภทได้ DSL ช่วยให้สร้างโครงสร้างข้อมูลแบบลำดับชั้นที่ซับซ้อนได้ในลักษณะที่ดูแลรักษาและอ่านได้ง่ายขึ้น
Jetpack Compose ใช้ DSL สำหรับ API บางรายการ เช่น LazyRow และ LazyColumn
@Composable fun MessageList(messages: List<Message>) { LazyColumn { // Add a single item as a header item { Text("Message List") } // Add list of messages items(messages) { message -> Message(message) } } }
Kotlin ช่วยให้สามารถใช้เครื่องมือสร้างที่ปลอดภัยต่อประเภทโดยใช้ฟังก์ชันลิเทอรัลที่มีรีซีฟเวอร์
หากเรายกตัวอย่าง Composable Canvas จะใช้ฟังก์ชันที่มี DrawScope เป็นตัวรับ onDraw: DrawScope.() -> Unit เป็นพารามิเตอร์ ซึ่งช่วยให้บล็อกโค้ดเรียกใช้ฟังก์ชันสมาชิกที่กำหนดไว้ใน DrawScope ได้
Canvas(Modifier.size(120.dp)) { // Draw grey background, drawRect function is provided by the receiver drawRect(color = Color.Gray) // Inset content by 10 pixels on the left/right sides // and 12 by the top/bottom inset(10.0f, 12.0f) { val quadrantSize = size / 2.0f // Draw a rectangle within the inset bounds drawRect( size = quadrantSize, color = Color.Red ) rotate(45.0f) { drawRect(size = quadrantSize, color = Color.Blue) } } }
ดูข้อมูลเพิ่มเติมเกี่ยวกับเครื่องมือสร้างที่ปลอดภัยในการกำหนดประเภทและ DSL ในเอกสารประกอบของ Kotlin
โครูทีน Kotlin
โครูทีนให้การสนับสนุนการเขียนโปรแกรมแบบอะซิงโครนัสในระดับภาษาใน Kotlin โครูทีนสามารถระงับการดำเนินการได้โดยไม่บล็อกชุดข้อความ UI ที่ปรับเปลี่ยนตามอุปกรณ์เป็นแบบอะซิงโครนัสโดยเนื้อแท้ และ Jetpack Compose แก้ปัญหานี้ด้วยการใช้โครูทีนที่ระดับ API แทนการใช้ Callback
Jetpack Compose มี API ที่ช่วยให้ใช้โครูทีนได้อย่างปลอดภัยภายในเลเยอร์ UI
ฟังก์ชัน rememberCoroutineScope จะแสดงผล CoroutineScope ซึ่งคุณสามารถใช้สร้างโครูทีนในเครื่องจัดการเหตุการณ์และเรียกใช้ API การระงับของ Compose ได้ ดูตัวอย่างต่อไปนี้โดยใช้ API animateScrollTo ของ ScrollState
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Create a new coroutine that scrolls to the top of the list // and call the ViewModel to load data composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }
โครูทีนจะเรียกใช้บล็อกโค้ดตามลำดับโดยค่าเริ่มต้น โครูทีนที่ทำงานอยู่ซึ่งเรียกใช้ฟังก์ชันระงับจะระงับการดำเนินการจนกว่าฟังก์ชันระงับจะแสดงผล ซึ่งจะเป็นเช่นนี้แม้ว่าฟังก์ชันระงับจะย้ายการดำเนินการไปยัง CoroutineDispatcher อื่นก็ตาม ในตัวอย่างก่อนหน้านี้
loadData จะไม่ทำงานจนกว่าฟังก์ชันระงับ animateScrollTo
จะแสดงผล
หากต้องการเรียกใช้โค้ดพร้อมกัน คุณต้องสร้าง Coroutine ใหม่ ในตัวอย่างด้านบน หากต้องการเลื่อนไปยังด้านบนของหน้าจอและโหลดข้อมูลจาก viewModel ไปพร้อมกัน คุณต้องใช้ Coroutine 2 รายการ
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Scroll to the top and load data in parallel by creating a new // coroutine per independent work to do composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }
โครูทีนช่วยให้รวม API แบบอะซิงโครนัสได้ง่ายขึ้น ในตัวอย่างต่อไปนี้ เราจะรวมตัวแก้ไข pointerInput เข้ากับ API ภาพเคลื่อนไหวเพื่อทำให้ตำแหน่งขององค์ประกอบเคลื่อนไหวเมื่อผู้ใช้แตะหน้าจอ
@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier .fillMaxSize() .pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen and animate // in the same block awaitPointerEventScope { val offset = awaitFirstDown().position // Launch a new coroutine to asynchronously animate to // where the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) }
ดูข้อมูลเพิ่มเติมเกี่ยวกับ Coroutine ได้ที่คู่มือ Kotlin Coroutine ใน Android
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- คอมโพเนนต์และเลย์เอาต์ของ Material
- ผลข้างเคียงใน Compose
- ข้อมูลเบื้องต้นเกี่ยวกับเลย์เอาต์ Compose