Kotlin untuk Jetpack Compose

Jetpack Compose dibuat berdasarkan Kotlin. Dalam beberapa kasus, Kotlin memberikan idiom khusus yang mempermudah penulisan kode Compose yang baik. Jika Anda memikirkan bahasa pemrograman lain dan secara natural menerjemahkan bahasa tersebut ke Kotlin, Anda mungkin akan melewatkan beberapa keunggulan Compose, dan mungkin akan merasa sulit memahami kode Kotlin yang ditulis secara idiomatis. Mengenal gaya Kotlin lebih jauh dapat membantu Anda menghindari kesulitan tersebut.

Argumen default

Saat menulis fungsi Kotlin, Anda dapat menentukan nilai default untuk argumen fungsi, yang digunakan jika pemanggil tidak secara eksplisit meneruskan nilai tersebut. Fitur ini mengurangi kebutuhan akan fungsi yang berlebihan.

Misalnya, Anda ingin menulis fungsi yang menggambar persegi. Fungsi tersebut mungkin memiliki satu parameter yang diperlukan, yaitu sideLength, yang menentukan panjang setiap sisi. Fungsi itu mungkin memiliki beberapa parameter opsional, seperti thickness, edgeColor, dan sebagainya; jika pemanggil tidak menentukannya, fungsi akan menggunakan nilai default. Dalam bahasa lain, Anda mungkin perlu menulis beberapa fungsi:

// 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) { }

Di Kotlin, Anda dapat menulis fungsi tunggal dan menentukan nilai default untuk argumen:

fun drawSquare(
    sideLength: Int,
    thickness: Int = 2,
    edgeColor: Color = Color.Black
) {
}

Selain mencegah Anda menulis beberapa fungsi secara berlebihan, fitur ini menjadikan kode Anda lebih mudah dibaca. Jika pemanggil tidak menentukan nilai untuk sebuah argumen, itu menunjukkan bahwa fungsi akan menggunakan nilai default. Selain itu, parameter yang dinamai mempermudah untuk melihat apa yang terjadi. Jika Anda melihat kode dan melihat panggilan fungsi seperti ini, Anda mungkin tidak tahu apa arti parameter itu tanpa memeriksa kode drawSquare():

drawSquare(30, 5, Color.Red);

Sebaliknya, kode ini mendokumentasikan dirinya sendiri:

drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)

Sebagian besar library Compose menggunakan argumen default, dan ada baiknya untuk melakukan hal yang sama pada fungsi yang dapat dikomposisi, yang Anda tulis. Praktik ini membuat komposisi Anda dapat disesuaikan, tetapi tetap membuat perilaku default mudah dipanggil. Jadi, misalnya, Anda dapat membuat elemen teks sederhana seperti ini:

Text(text = "Hello, Android!")

Kode tersebut memiliki efek yang sama seperti kode berikut yang jauh lebih panjang, dengan lebih banyak parameter Text yang ditetapkan secara eksplisit:

Text(
    text = "Hello, Android!",
    color = Color.Unspecified,
    fontSize = TextUnit.Unspecified,
    letterSpacing = TextUnit.Unspecified,
    overflow = TextOverflow.Clip
)

Cuplikan kode pertama tidak hanya jauh lebih sederhana dan lebih mudah dibaca, tetapi juga mendokumentasikan dirinya sendiri. Dengan hanya menentukan parameter text, Anda mendokumentasikan bahwa untuk semua parameter lainnya, Anda ingin menggunakan nilai default. Sebaliknya, cuplikan kedua menyiratkan bahwa Anda ingin menetapkan nilai secara eksplisit untuk parameter lainnya, meskipun nilai yang Anda tetapkan kebetulan adalah nilai default untuk fungsi tersebut.

Fungsi dan ekspresi lambda yang lebih tinggi

Kotlin mendukung fungsi yang lebih tinggi, yang menerima fungsi lain sebagai parameter. Compose dibuat berdasarkan pendekatan ini. Misalnya, fungsi Button yang dapat dikomposisi menyediakan parameter lambda onClick. Nilai parameter tersebut adalah fungsi, yang dipanggil oleh tombol saat pengguna mengkliknya:

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

Fungsi yang lebih tinggi berpasangan secara alami dengan ekspresi lambda, yaitu ekspresi yang mengevaluasi sebuah fungsi. Jika Anda hanya memerlukan fungsi tersebut sekali, Anda tidak perlu menentukannya di tempat lain untuk meneruskannya ke fungsi yang lebih tinggi. Sebagai gantinya, Anda dapat menentukan fungsi secara langsung dengan ekspresi lambda. Contoh sebelumnya mengasumsikan bahwa myClickFunction() ditentukan di tempat lain. Namun, jika Anda hanya menggunakan fungsi tersebut di sini, akan lebih mudah untuk menentukan fungsi itu sebagai bagian dari ekspresi lambda:

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

Lambda akhir

Kotlin menawarkan sintaksis khusus untuk memanggil fungsi yang lebih tinggi dengan parameter terakhir adalah lambda. Jika Anda ingin meneruskan ekspresi lambda sebagai parameter tersebut, Anda dapat menggunakan sintaksis lambda akhir. Alih-alih menempatkan ekspresi lambda dalam tanda kurung, Anda harus menempatkannya setelahnya. Ini adalah situasi umum di Compose, jadi Anda harus mengetahui tampilan kodenya.

Misalnya, parameter terakhir untuk semua tata letak, seperti fungsi Column() yang dapat dikomposisi, adalah content, yaitu fungsi yang membuat elemen UI turunan. Misalnya Anda ingin membuat kolom yang berisi tiga elemen teks, dan Anda perlu menerapkan beberapa pemformatan. Kode ini akan berfungsi, tetapi sangat rumit:

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

Karena parameter content adalah parameter terakhir dalam tanda tangan fungsi, dan kita meneruskan nilainya sebagai ekspresi lambda, kita dapat mengeluarkannya dari tanda kurung:

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

Kedua contoh tersebut memiliki arti yang sama persis. Kurung kurawal menentukan ekspresi lambda yang diteruskan ke parameter content.

Sebenarnya, jika satu-satunya parameter yang Anda teruskan adalah lambda akhir—yaitu, jika parameter terakhir adalah lambda, dan Anda tidak meneruskan parameter lain—Anda bisa menghapus tanda kurung sekaligus. Jadi, misalnya, Anda tidak perlu meneruskan pengubah ke Column. Anda dapat menulis kode seperti ini:

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

Sintaksis ini sangat umum di Compose, terutama untuk elemen tata letak seperti Column. Parameter terakhir adalah ekspresi lambda yang menentukan turunan elemen, dan turunan tersebut ditentukan dalam tanda kurung kurawal setelah panggilan fungsi.

Cakupan dan penerima

Beberapa metode dan properti hanya tersedia dalam cakupan tertentu. Cakupan terbatas memungkinkan Anda menawarkan fungsi di tempat yang memerlukannya dan menghindari penggunaan fungsi tersebut secara tidak sengaja jika tidak sesuai.

Pertimbangkan contoh yang digunakan di Compose. Saat Anda memanggil komposisi tata letak Row, lambda konten Anda akan dipanggil secara otomatis dalam RowScope. Ini memungkinkan Row mengekspos fungsi yang hanya valid dalam Row. Contoh di bawah menunjukkan cara Row mengekspos nilai spesifik baris untuk pengubah 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)
    )
}

Beberapa API menerima lambda yang dipanggil dalam cakupan penerima. Lambda tersebut memiliki akses ke properti dan fungsi yang ditentukan di tempat lain, berdasarkan deklarasi parameter:

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(
            /*...*/
            /* ...
        )
    }
)

Untuk informasi selengkapnya, lihat literal fungsi dengan penerima dalam dokumentasi Kotlin.

Properti yang didelegasikan

Kotlin mendukung properti yang didelegasikan. Properti ini dipanggil seolah-olah merupakan kolom, tetapi nilainya ditentukan secara dinamis dengan mengevaluasi ekspresi. Anda dapat mengenali properti ini dari penggunaan sintaksis by:

class DelegatingClass {
    var name: String by nameGetterFunction()

    // ...
}

Kode lain dapat mengakses properti dengan kode seperti ini:

val myDC = DelegatingClass()
println("The name property is: " + myDC.name)

Saat println() dieksekusi, nameGetterFunction() dipanggil untuk menampilkan nilai string.

Properti yang didelegasikan ini sangat berguna ketika Anda bekerja dengan properti yang didukung status:

var showDialog by remember { mutableStateOf(false) }

// Updating the var automatically triggers a state change
showDialog = true

Destrukturisasi class data

Jika Anda menentukan class data, Anda dapat dengan mudah mengakses data dengan deklarasi destrukturisasi. Misalnya, anggaplah Anda menentukan class Person:

data class Person(val name: String, val age: Int)

Jika Anda memiliki objek dari jenis tersebut, Anda dapat mengakses nilainya dengan kode seperti ini:

val mary = Person(name = "Mary", age = 35)

// ...

val (name, age) = mary

Anda akan sering melihat kode semacam itu di fungsi 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.

    // ...
}

Class data menyediakan banyak fungsi berguna lainnya. Misalnya, saat Anda menentukan class data, compiler akan secara otomatis menentukan fungsi yang berguna seperti equals() dan copy(). Anda dapat menemukan informasi selengkapnya di dokumentasi class data.

Objek singleton

Kotlin memudahkan untuk mendeklarasikan class singleton yang selalu memiliki satu instance saja. Objek singleton ini dideklarasikan dengan kata kunci object. Compose sering menggunakan objek tersebut. Misalnya, MaterialTheme didefinisikan sebagai objek singleton; properti MaterialTheme.colors, shapes, dan typography semuanya berisi nilai untuk tema saat ini.

Builder jenis aman dan DSL

Kotlin memungkinkan pembuatan domain-specific language (DSL) dengan builder jenis aman. DSL memungkinkan pembuatan struktur data hierarki kompleks dengan cara yang lebih mudah dikelola dan lebih mudah dibaca.

Jetpack Compose menggunakan DSL untuk beberapa API, seperti LazyRow dan 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 menjamin builder jenis aman menggunakan literal fungsi dengan penerima. Jika menggunakan fungsi Canvas yang dapat dikomposisi sebagai contoh, fungsi dengan DrawScope sebagai penerima akan digunakan sebagai parameter, onDraw: DrawScope.() -> Unit, sehingga memungkinkan blok kode memanggil fungsi anggota yang ditentukan di 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)
        }
    }
}

Pelajari lebih lanjut builder jenis aman dan DSL di dokumentasi Kotlin.

Coroutine Kotlin

Coroutine menawarkan dukungan pemrograman asinkron pada tingkat bahasa di Kotlin. Coroutine dapat menangguhkan eksekusi tanpa memblokir thread. UI responsif pada dasarnya asinkron, dan Jetpack Compose memecahkan masalah ini dengan menggunakan coroutine pada API level, bukan menggunakan callback.

Jetpack Compose menawarkan API yang menjadikan penggunaan coroutine aman dalam lapisan UI. Fungsi rememberCoroutineScope menampilkan CoroutineScope yang dapat digunakan untuk membuat coroutine di pengendali peristiwa dan memanggil API penangguhan Compose. Lihat contoh di bawah menggunakan 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 secara default mengeksekusi blok kode secara berurutan. Coroutine yang berjalan dan memanggil fungsi penangguhan menangguhkan eksekusinya hingga fungsi penangguhan ditampilkan. Hal ini benar meskipun fungsi penangguhan memindahkan eksekusi ke berbagai CoroutineDispatcher. Pada contoh sebelumnya, loadData tidak akan dieksekusi hingga fungsi penangguhan animateScrollTo ditampilkan.

Untuk mengeksekusi kode secara berurutan, coroutine baru harus dibuat. Pada contoh di atas, untuk memparalelkan scroll ke bagian atas layar dan memuat data dari viewModel, dua coroutine diperlukan.

// 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 memudahkan penggabungan API asinkron. Pada contoh berikut, kita menggabungkan pengubah pointerInput dengan API animasi untuk menganimasikan posisi elemen saat pengguna mengetuk layar.

@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)
        )
    }

Untuk mempelajari Coroutine lebih lanjut, lihat panduan Coroutine Kotlin di Android.