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 ส่วนใหญ่ใช้อาร์กิวเมนต์เริ่มต้น และคุณควรทำเช่นเดียวกันกับฟังก์ชันคอมโพสิเบิลที่คุณเขียน แนวทางนี้ช่วยให้คอมโพสิเบิลปรับแต่งได้ แต่ยังคงเรียกใช้ลักษณะการทำงานเริ่มต้นได้ง่ายๆ ตัวอย่างเช่น คุณอาจสร้างองค์ประกอบข้อความง่ายๆ ดังนี้
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 รองรับฟังก์ชันระดับสูง ซึ่งเป็นฟังก์ชันที่รับฟังก์ชันอื่นๆ เป็นพารามิเตอร์ คอมโพเซ่นจะต่อยอดจากแนวทางนี้ ตัวอย่างเช่น ฟังก์ชันคอมโพสิเบิล Button
จะมีพารามิเตอร์ LAMBDA onClick
ค่าของพารามิเตอร์นั้นคือฟังก์ชันที่ปุ่มเรียกใช้เมื่อผู้ใช้คลิก
Button( // ... onClick = myClickFunction ) // ...
ฟังก์ชันระดับสูงจะจับคู่กับนิพจน์ 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
อันที่จริงแล้ว หากพารามิเตอร์เดียวที่คุณส่งคือแลมดาที่อยู่ท้าย นั่นคือ หากพารามิเตอร์สุดท้ายคือแลมดาและคุณไม่ได้ส่งพารามิเตอร์อื่นใดเลย คุณก็ละเว้นวงเล็บไปเลยได้ ตัวอย่างเช่น สมมติว่าคุณไม่จำเป็นต้องส่งตัวแก้ไขไปยัง Column
คุณอาจเขียนโค้ดดังนี้
Column { Text("Some text") Text("Some more text") Text("Last text") }
ไวยากรณ์นี้พบได้บ่อยในเครื่องมือเขียน โดยเฉพาะอย่างยิ่งสำหรับองค์ประกอบเลย์เอาต์ เช่น Column
พารามิเตอร์สุดท้ายคือนิพจน์ Lambda ที่กําหนดองค์ประกอบย่อยขององค์ประกอบ และองค์ประกอบย่อยเหล่านั้นจะระบุไว้ในวงเล็บเหลี่ยมหลังการเรียกใช้ฟังก์ชัน
กล้องและตัวรับ
เมธอดและพร็อพเพอร์ตี้บางรายการใช้ได้เฉพาะในขอบเขตที่เฉพาะเจาะจงเท่านั้น ขอบเขตที่จำกัดช่วยให้คุณนำเสนอฟังก์ชันการทำงานได้ตรงกับความต้องการและหลีกเลี่ยงการใช้ฟังก์ชันการทำงานนั้นโดยไม่ตั้งใจในสถานการณ์ที่ไม่เหมาะสม
มาดูตัวอย่างที่ใช้ใน "เขียน" เมื่อคุณเรียกใช้Row
layout
composable ระบบจะเรียกใช้ Lambda ของเนื้อหาโดยอัตโนมัติภายใน 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
คุณมักจะเห็นโค้ดประเภทนี้ในฟังก์ชันการเขียน
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 ช่วยให้คุณประกาศแบบสแตนด์อโลน ซึ่งเป็นคลาสที่มีอินสแตนซ์เพียงรายการเดียวเสมอได้อย่างง่ายดาย โดยประกาศตัวแปรเดี่ยวเหล่านี้ด้วยคีย์เวิร์ด object
คอมโพสิชันมักใช้ออบเจ็กต์ดังกล่าว ตัวอย่างเช่น MaterialTheme
ได้รับการกําหนดให้เป็นออบเจ็กต์แบบ Singleton โดยพร็อพเพอร์ตี้ 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 รับประกันว่าบิลเดอร์จะเป็นประเภทที่ปลอดภัยโดยใช้นิพจน์ฟังก์ชันที่มีตัวรับ
หากใช้ 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
Coroutine ของ Kotlin
Coroutines รองรับการเขียนโปรแกรมแบบแอซิงโครนัสที่ระดับภาษาใน Kotlin Coroutine สามารถหยุดชั่วคราวการดำเนินการได้โดยไม่ต้องบล็อกเธรด UI แบบตอบสนองจะทำงานแบบไม่พร้อมกันโดยพื้นฐาน และ Jetpack Compose แก้ปัญหานี้ด้วยการรองรับ coroutine ที่ระดับ API แทนการใช้การเรียกกลับ
Jetpack Compose มี API ที่ทำให้การใช้ Coroutine ปลอดภัยภายในเลเยอร์ UI
ฟังก์ชัน rememberCoroutineScope
จะแสดงผล CoroutineScope
ซึ่งคุณใช้สร้างโคโรทีนได้ในตัวแฮนเดิลเหตุการณ์และเรียกใช้ Compose Suspend API ดูตัวอย่างด้านล่างโดยใช้ animateScrollTo
API ของ 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() } } ) { /* ... */ }
Coroutine จะเรียกใช้บล็อกโค้ดตามลําดับโดยค่าเริ่มต้น โคโริวรีนที่กำลังทำงานซึ่งเรียกใช้ฟังก์ชันการระงับจะระงับการดำเนินการจนกว่าฟังก์ชันการระงับจะแสดงผล ซึ่งจะเป็นเช่นนี้แม้ในกรณีที่ฟังก์ชัน "หยุดชั่วคราว" จะย้ายการดําเนินการไปยัง CoroutineDispatcher
อื่น ในตัวอย่างก่อนหน้านี้ ระบบจะไม่เรียกใช้ loadData
จนกว่าฟังก์ชันการระงับ animateScrollTo
จะแสดงผล
หากต้องการเรียกใช้โค้ดพร้อมกัน คุณต้องสร้างโคโรทีนใหม่ ในตัวอย่างข้างต้น หากต้องการเลื่อนขึ้นด้านบนของหน้าจอและโหลดข้อมูลจาก 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() } } ) { /* ... */ }
Coroutine ช่วยให้รวม 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 val offset = awaitPointerEventScope { 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) ) }
ดูข้อมูลเพิ่มเติมเกี่ยวกับ Coroutines ได้ที่คู่มือCoroutines ของ Kotlin ใน Android
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- คอมโพเนนต์และเลย์เอาต์ของ Material Design
- ผลข้างเคียงในเครื่องมือเขียน
- ข้อมูลเบื้องต้นเกี่ยวกับการจัดวาง