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 = "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 } ) { /* ... */ }
Lambda ต่อท้าย
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 ระบบจะเรียกใช้ 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 ได้ง่าย ซึ่งเป็นคลาสที่มีอินสแตนซ์เดียวเท่านั้น
เสมอ 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
เป็นตัวอย่าง ฟังก์ชันนี้จะใช้ฟังก์ชันที่มี
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 ซึ่งคุณใช้สร้างโครูทีนในตัวจัดการเหตุการณ์และเรียกใช้
Compose API แบบระงับได้ ดูตัวอย่างด้านล่างโดยใช้
ScrollState's
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() } } ) { /* ... */ }
โครูทีนจะเรียกใช้บล็อกโค้ด ตามลำดับ โดยค่าเริ่มต้น โครูทีนที่กำลังทำงานซึ่งเรียกใช้ฟังก์ชันแบบระงับได้จะ ระงับ การทำงานจนกว่าฟังก์ชันแบบระงับได้จะแสดงผล ซึ่งจะเป็นเช่นนี้แม้ว่าฟังก์ชันแบบระงับได้จะย้ายการทำงานไปยัง CoroutineDispatcher อื่นก็ตาม ในตัวอย่างก่อนหน้านี้ ระบบจะไม่เรียกใช้ loadData จนกว่าฟังก์ชันแบบระงับได้ animateScrollTo จะแสดงผล
หากต้องการเรียกใช้โค้ดพร้อมกัน คุณต้องสร้างโครูทีนใหม่ ในตัวอย่างด้านบน หากต้องการเลื่อนไปที่ด้านบนของหน้าจอและโหลดข้อมูลจาก viewModel ไปพร้อมๆ กัน คุณต้องใช้โครูทีน 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) ) }
ดูข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนได้ใน คู่มือโครูทีน Kotlin ใน Android
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- คอมโพเนนต์และเลย์เอาต์เนื้อหา
- ผลข้างเคียงใน Compose
- ข้อมูลเบื้องต้นเกี่ยวกับเลย์เอาต์ Compose