Kotlin สำหรับ Jetpack Compose

Jetpack Compose สร้างขึ้นจาก Kotlin ในบางกรณี Kotlin จะจัดเตรียมคุณลักษณะ สำนวนที่ช่วยให้เขียนโค้ดสำหรับ Compose ที่ดีได้ง่ายขึ้น หากคุณคิดในอีก ภาษาโปรแกรม แล้วแปลภาษานั้นเป็นภาษา Kotlin ด้วยความคิด มีแนวโน้มที่จะพลาดประสิทธิภาพบางอย่างของ Compose และคุณอาจพบ โค้ด Kotlin ที่เขียนด้วยสำนวนเข้าใจยาก เพิ่มขึ้น ความคุ้นเคยกับสไตล์ของ Kotlin จะช่วยให้คุณหลีกเลี่ยงข้อผิดพลาดเหล่านั้นได้

อาร์กิวเมนต์เริ่มต้น

เมื่อเขียนฟังก์ชัน Kotlin คุณสามารถระบุค่าเริ่มต้นสําหรับฟังก์ชันได้ อาร์กิวเมนต์ ใช้ในกรณีที่ผู้โทรไม่ได้ส่งผ่านค่าเหล่านั้นอย่างชัดเจน ฟีเจอร์นี้จะลด เมื่อต้องใช้ฟังก์ชันมากเกินไป

เช่น สมมติว่าคุณต้องการเขียนฟังก์ชันที่วาดรูปสี่เหลี่ยมจัตุรัส นั่น อาจมีพารามิเตอร์เดียวที่จำเป็นคือ sideLength ซึ่งระบุความยาว ของแต่ละด้าน ซึ่งอาจมีพารามิเตอร์ที่ไม่บังคับหลายรายการ เช่น ความหนา 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 หมายความว่าคุณบันทึกว่า พารามิเตอร์อื่นๆ ทั้งหมด คุณควรเลือกใช้ค่าเริ่มต้น ในทางตรงกันข้าม ตัวอย่างที่สองแสดงว่าคุณต้องการกำหนดค่าสำหรับส่วนขยายเหล่านั้น พารามิเตอร์อื่นๆ แม้ว่าค่าที่คุณตั้งจะเป็นค่าเริ่มต้นสำหรับ ฟังก์ชัน

ฟังก์ชันลำดับสูงกว่าและนิพจน์แลมบ์ดา

Kotlin รองรับลำดับที่สูงกว่า หมายถึงฟังก์ชันที่ รับฟังก์ชันอื่นๆ เป็นพารามิเตอร์ การเขียนต่อยอดมาจากวิธีการนี้ สำหรับ ตัวอย่างเช่น Button ฟังก์ชัน Composable จะมีพารามิเตอร์ lambda onClick ค่า ของพารามิเตอร์นั้นคือฟังก์ชัน ซึ่งปุ่มเรียกใช้เมื่อผู้ใช้คลิกปุ่ม:

Button(
    // ...
    onClick = myClickFunction
)
// ...

ฟังก์ชันที่มีลำดับสูงกว่าจะจับคู่อย่างเป็นธรรมชาติกับนิพจน์แลมบ์ดา นิพจน์อื่นๆ ซึ่งประเมินผลฟังก์ชันได้ หากต้องการฟังก์ชันนี้เพียงครั้งเดียว คุณก็ไม่ต้อง เพื่อกำหนดไว้ที่อื่นเพื่อส่งไปยังฟังก์ชันที่มีลำดับสูงกว่า อย่างไรก็ตาม คุณสามารถ ก็แค่กำหนดฟังก์ชันตรงนั้นด้วยนิพจน์แลมบ์ดา ตัวอย่างก่อนหน้านี้ ถือว่า myClickFunction() ถูกกำหนดไว้ที่อื่น แต่ถ้าคุณใช้แค่นั้น ฟังก์ชันที่นี่ จะให้นิยามฟังก์ชันในบรรทัดด้วย lambda ได้ง่ายขึ้น นิพจน์:

Button(
    // ...
    onClick = {
        // do something
        // do something else
    }
) { /* ... */ }

แลมบ์ดาที่ตามหลัง

Kotlin มีไวยากรณ์พิเศษสำหรับการเรียกใช้ฟังก์ชันที่มีลำดับสูงกว่าซึ่งคำสั่งท้ายสุด คือ lambda ถ้าอยากส่งนิพจน์แลมบ์ดาว่า คุณสามารถใช้ trailing lambda แทนที่จะใส่นิพจน์ lambda ภายในวงเล็บ คุณก็ใส่ข้อความ หลังจากนั้น นี่เป็นสถานการณ์ทั่วไปในการเขียน คุณจึงต้องทำความคุ้นเคย กับลักษณะของโค้ด

ตัวอย่างเช่น พารามิเตอร์สุดท้ายกับทุกเลย์เอาต์ เช่น พารามิเตอร์ Column() ฟังก์ชัน Composable คือ content ซึ่งเป็นฟังก์ชันที่แสดง UI ย่อย จากองค์ประกอบเหล่านี้ สมมติว่าคุณต้องการสร้างคอลัมน์ที่มีองค์ประกอบข้อความ 3 รายการ และจำเป็นต้องใช้การจัดรูปแบบบางอย่าง โค้ดนี้ใช้งานได้ แต่ ยุ่งยาก:

Column(
    modifier = Modifier.padding(16.dp),
    content = {
        Text("Some text")
        Text("Some more text")
        Text("Last text")
    }
)

เนื่องจากพารามิเตอร์ content เป็นค่าสุดท้ายในลายเซ็นของฟังก์ชัน และ เรากำลังส่งผ่านค่าเป็นนิพจน์แลมบ์ดา เราสามารถดึงค่าออกมาจาก วงเล็บ:

Column(modifier = Modifier.padding(16.dp)) {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

ทั้ง 2 ตัวอย่างมีความหมายเหมือนกัน วงเล็บปีกกาหมายถึงเครื่องหมายแลมบ์ดา นิพจน์ที่ส่งไปยังพารามิเตอร์ content

ที่จริงแล้ว หากพารามิเตอร์เดียวที่คุณส่งคือ lambda ที่ต่อท้าย นั่นคือ หากพารามิเตอร์สุดท้ายคือ lambda และคุณไม่ได้ส่งผ่านพารามิเตอร์อื่น คุณสามารถข้ามวงเล็บได้ ตัวอย่างเช่น สมมติว่าคุณ ไม่จำเป็นต้องส่งคีย์ตัวปรับแต่งให้ Column คุณสามารถเขียนโค้ดอย่างเช่น ดังนี้

Column {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

รูปแบบคำสั่งนี้ค่อนข้างพบบ่อยในการเขียน โดยเฉพาะสำหรับองค์ประกอบการออกแบบ เช่น Column พารามิเตอร์สุดท้ายคือนิพจน์ lambda ที่กำหนดพารามิเตอร์ เด็ก และเด็กเหล่านี้จะมีเครื่องหมายวงเล็บปีกกาหลังจากการเรียกใช้ฟังก์ชัน

สโคปและตัวรับสัญญาณ

บางเมธอดและพร็อพเพอร์ตี้บางอย่างจะใช้ได้ในบางขอบเขตเท่านั้น ข้อจำกัด ขอบเขตช่วยให้คุณสามารถนำเสนอฟังก์ชันเมื่อจำเป็น และหลีกเลี่ยง ใช้ฟังก์ชันนั้นในทางที่ไม่เหมาะสม

ลองดูตัวอย่างที่ใช้ใน Compose เมื่อคุณเรียกใช้เลย์เอาต์ Row 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 คือ เป็นออบเจ็กต์เดี่ยว 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

Coroutines เสนอการสนับสนุนการเขียนโปรแกรมแบบไม่พร้อมกันในระดับภาษาใน Kotlin Coroutine จะระงับการดำเนินการโดยไม่บล็อกเทรดได้ ต UI ที่ตอบสนองตามอุปกรณ์จะเป็นแบบไม่พร้อมกันโดยธรรมชาติ และ Jetpack Compose แก้ปัญหานี้ได้โดย ใช้ Coroutine ที่ระดับ API แทนการใช้ Callback

Jetpack Compose มี API ที่ทำให้การใช้ Coroutine ปลอดภัยภายในเลเยอร์ UI rememberCoroutineScope จะแสดงผล CoroutineScope ที่คุณสามารถสร้างโครูทีนในเครื่องจัดการเหตุการณ์และการเรียกใช้ เขียน API ที่ระงับ ดูตัวอย่างด้านล่างโดยใช้ ของ 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 จะเรียกใช้บล็อกโค้ดตามลำดับโดยค่าเริ่มต้น การวิ่ง Coroutine ที่เรียกใช้ฟังก์ชันระงับจะระงับการดำเนินการจนกว่าฟังก์ชัน ฟังก์ชันระงับการส่งคืน ถึงแม้ฟังก์ชันระงับจะย้ายฟังก์ชัน ไปยัง 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()
        }
    }
) { /* ... */ }

Coroutines ช่วยให้รวม 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 ได้ที่ คำแนะนำเกี่ยวกับโครูทีน Kotlin ใน Android