Kotlin สำหรับ Jetpack Compose

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 ที่คุณเขียน วิธีนี้ช่วยให้ Composables ปรับแต่งได้ แต่ยังคงทำให้เรียกใช้ลักษณะการทำงานเริ่มต้นได้ง่าย เช่น คุณอาจสร้างองค์ประกอบของข้อความอย่างง่ายดังนี้

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 จะมีพารามิเตอร์ onClick Lambda ค่าของพารามิเตอร์นั้นคือฟังก์ชันที่ปุ่มเรียกใช้เมื่อผู้ใช้คลิกปุ่ม

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

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

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

Lambda ต่อท้าย

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

หากต้องการเรียกใช้โค้ดพร้อมกัน คุณต้องสร้างโครูทีนใหม่ ในตัวอย่าง ด้านบน คุณต้องใช้โครูทีน 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 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)
        )
    }

ดูข้อมูลเพิ่มเติมเกี่ยวกับ Coroutines ได้ที่คำแนะนำเกี่ยวกับ Kotlin Coroutines ใน Android