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 ส่วนใหญ่ใช้อาร์กิวเมนต์เริ่มต้น และควรทำ เช่นเดียวกันกับฟังก์ชันที่ใช้ 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 หมายความว่าคุณต้องการตั้งค่าสำหรับพารามิเตอร์อื่นๆ เหล่านั้นอย่างชัดเจน แม้ว่าค่าที่คุณตั้งจะเป็นค่าเริ่มต้นสำหรับฟังก์ชันก็ตาม
ฟังก์ชันลำดับสูงและนิพจน์แลมบ์ดา
Kotlin รองรับฟังก์ชันลำดับสูง ซึ่งเป็นฟังก์ชันที่รับฟังก์ชันอื่นๆ เป็นพารามิเตอร์ Compose สร้างขึ้นจากแนวทางนี้ ตัวอย่างเช่น ฟังก์ชันที่ประกอบได้ Button
มีพารามิเตอร์แลมบ์ดา onClick
ค่า
ของพารามิเตอร์นั้นคือฟังก์ชัน ซึ่งปุ่มจะเรียกใช้เมื่อผู้ใช้คลิกปุ่ม
Button( // ... onClick = myClickFunction ) // ...
ฟังก์ชันลำดับสูงจะทำงานร่วมกับนิพจน์แลมบ์ดาได้อย่างเป็นธรรมชาติ ซึ่งเป็นนิพจน์
ที่ประเมินค่าเป็นฟังก์ชัน หากต้องการใช้ฟังก์ชันเพียงครั้งเดียว คุณไม่จำเป็นต้องกำหนดฟังก์ชันที่อื่นเพื่อส่งไปยังฟังก์ชันลำดับสูงกว่า แต่คุณสามารถ
กำหนดฟังก์ชันตรงนั้นได้เลยด้วยนิพจน์แลมบ์ดา ตัวอย่างก่อนหน้า
ถือว่ามีการกำหนด myClickFunction()
ไว้ที่อื่น แต่หากคุณใช้ฟังก์ชันนั้นที่นี่เท่านั้น การกำหนดฟังก์ชันแบบอินไลน์ด้วยนิพจน์ Lambda จะง่ายกว่า
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
Trailing lambdas
Kotlin มีไวยากรณ์พิเศษสำหรับการเรียกฟังก์ชันลำดับสูงซึ่งพารามิเตอร์สุดท้าย เป็น Lambda หากต้องการส่งนิพจน์ 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
ในความเป็นจริง หากพารามิเตอร์ only ที่คุณส่งคือ Lambda ต่อท้าย นั่นคือ
หากพารามิเตอร์สุดท้ายเป็น Lambda และคุณไม่ได้ส่งพารามิเตอร์อื่นๆ
คุณก็ละเว้นวงเล็บทั้งหมดได้ เช่น สมมติว่าคุณ
ไม่จำเป็นต้องส่งตัวแก้ไขไปยัง Column
คุณอาจเขียนโค้ดดังนี้
Column { Text("Some text") Text("Some more text") Text("Last text") }
ไวยากรณ์นี้พบได้ทั่วไปใน Compose โดยเฉพาะสำหรับองค์ประกอบเลย์เอาต์ เช่น
Column
พารามิเตอร์สุดท้ายคือนิพจน์ Lambda ที่กำหนดองค์ประกอบ
ลูก และระบุองค์ประกอบลูกเหล่านั้นในวงเล็บปีกกาหลังการเรียกฟังก์ชัน
ขอบเขตและตัวรับ
บางเมธอดและพร็อพเพอร์ตี้ใช้ได้ในขอบเขตที่เฉพาะเจาะจงเท่านั้น ขอบเขตที่จำกัด ช่วยให้คุณเสนอฟังก์ชันการทำงานในที่ที่จำเป็นและหลีกเลี่ยงการใช้ฟังก์ชันการทำงานนั้นโดยไม่ตั้งใจในที่ที่ไม่เหมาะสม
ลองดูตัวอย่างที่ใช้ใน Compose เมื่อเรียกใช้ Composable ของเลย์เอาต์ Row
ระบบจะเรียกใช้ 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 ซึ่งจะเรียกใช้ในขอบเขตตัวรับ 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
จะ
กำหนดเป็นออบเจ็กต์ 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
composable เป็นตัวอย่าง ฟังก์ชันนี้จะใช้ฟังก์ชันที่มี
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 แก้ปัญหานี้ด้วยการ ใช้ Coroutine ที่ระดับ API แทนการใช้ Callback
Jetpack Compose มี API ที่ทำให้การใช้โครูทีนในเลเยอร์ UI ปลอดภัย
ฟังก์ชัน rememberCoroutineScope
จะแสดงผล CoroutineScope
ที่คุณใช้สร้างโครูทีนในตัวแฮนเดิลเหตุการณ์และเรียก
API แบบระงับของ Compose ได้ ดูตัวอย่างด้านล่างโดยใช้
ScrollState
animateScrollTo
API
// 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
จะแสดงผล
หากต้องการเรียกใช้โค้ดพร้อมกัน คุณต้องสร้างโครูทีนใหม่ ในตัวอย่าง
ด้านบน คุณต้องใช้โครูทีน 2 รายการเพื่อเลื่อนแบบขนานไปที่ด้านบนของหน้าจอและโหลดข้อมูลจาก
viewModel
// 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
เข้ากับ Animation 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 ได้ที่คู่มือKotlin Coroutines ใน Android
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- คอมโพเนนต์และเลย์เอาต์ของ Material
- ผลข้างเคียงในฟีเจอร์เขียน
- ข้อมูลเบื้องต้นเกี่ยวกับเลย์เอาต์การเขียน