Dasar-dasar tata letak Compose

Jetpack Compose mempermudah desain dan pembuatan UI aplikasi Anda. Compose mengubah status menjadi elemen UI, melalui:

  1. Komposisi elemen
  2. Tata letak elemen
  3. Gambar elemen

Compose mengubah status ke UI melalui komposisi, tata letak, gambar

Dokumen ini berfokus pada tata letak elemen, yang menjelaskan beberapa elemen penyusun yang disediakan Compose untuk membantu Anda menata elemen UI.

Sasaran tata letak di Compose

Implementasi Jetpack Compose sistem tata letak memiliki dua sasaran utama:

Dasar-dasar Fungsi yang dapat dikomposisi

Fungsi yang dapat dikomposisi adalah elemen dasar penyusun Compose. Fungsi yang dapat dikomposisi adalah fungsi yang menampilkan Unit yang mendeskripsikan beberapa bagian UI Anda. Fungsi ini mengambil beberapa input dan menghasilkan apa yang ditampilkan di layar. Untuk mengetahui informasi selengkapnya tentang composable, lihat dokumentasi Model mental Compose.

Fungsi yang dapat dikomposisi dapat membuat beberapa elemen UI. Namun, jika Anda tidak memberikan panduan tentang cara mengaturnya, Compose mungkin akan mengatur elemen dengan cara yang tidak Anda sukai. Misalnya, kode ini menghasilkan dua elemen teks:

@Composable
fun ArtistCard() {
    Text("Alfred Sisley")
    Text("3 minutes ago")
}

Tanpa panduan tentang cara Anda ingin mengaturnya, Compose akan menumpuk elemen teks di atas satu sama lain, sehingga tidak dapat dibaca:

Dua elemen teks digambar di atas satu sama lain, membuat teks tidak dapat dibaca

Compose menyediakan kumpulan tata letak yang siap pakai untuk membantu Anda mengatur elemen UI, dan memudahkan Anda menentukan tata letak sendiri yang lebih khusus.

Komponen tata letak standar

Sering kali, Anda cukup menggunakan elemen tata letak standar Compose.

Gunakan Column untuk menempatkan item secara vertikal di layar.

@Composable
fun ArtistCard() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

Dua elemen teks disusun dalam tata letak kolom, sehingga teks dapat dibaca

Demikian pula, gunakan Row untuk menempatkan item secara horizontal di layar. Baik Column maupun Row mendukung konfigurasi penyelarasan elemen di dalamnya.

@Composable
fun ArtistCard(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(/*...*/)
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

Menampilkan tata letak yang lebih kompleks, dengan grafik kecil di samping kolom elemen teks

Gunakan Box untuk menempatkan elemen di atas elemen lainnya. Box juga mendukung konfigurasi perataan tertentu dari elemen yang ada di dalamnya.

@Composable
fun ArtistAvatar(artist: Artist) {
    Box {
        Image(/*...*/)
        Icon(/*...*/)
    }
}

Menampilkan dua elemen yang bertumpuk satu sama lain

Terkadang, hanya elemen penyusun inilah yang Anda butuhkan. Anda dapat menulis fungsi yang dapat dikomposisi sendiri untuk menggabungkan tata letak ini ke dalam tata letak yang lebih rumit, yang sesuai dengan aplikasi Anda.

Membandingkan tiga komponen tata letak sederhana: kolom, baris, dan kotak

Untuk menyetel posisi turunan dalam Row, setel argumen horizontalArrangement dan verticalAlignment. Untuk Column, setel argumen verticalArrangement dan horizontalAlignment:

@Composable
fun ArtistCard(artist: Artist) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.End
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

Item diratakan di sisi kanan

Model tata letak

Pada model tata letak, hierarki UI ditata dalam satu penerusan. Setiap node pertama-tama diminta untuk mengukur dirinya sendiri, lalu mengukur setiap turunan secara rekursif, yang meneruskan batasan ukuran ke hierarki bawah pada turunan. Lalu, node daun diukur dan ditempatkan, dengan ukuran dan petunjuk penempatan yang telah di-resolve diteruskan kembali ke hierarki atas.

Singkatnya, induk diukur sebelum turunannya, tetapi mendapatkan pengubahan ukuran dan penempatan setelah turunannya.

Perhatikan fungsi SearchResult berikut.

@Composable
fun SearchResult(...) {
  Row(...) {
    Image(...)
    Column(...) {
      Text(...)
      Text(..)
    }
  }
}

Fungsi ini menghasilkan hierarki UI berikut.

SearchResult
  Row
    Image
    Column
      Text
      Text

Dalam contoh SearchResult, tata letak hierarki UI mengikuti urutan ini:

  1. Node root Row diminta untuk mengukur.
  2. Node root Row meminta pengukuran turunan pertamanya, Image.
  3. Image adalah node daun (yaitu yang tidak memiliki turunan), sehingga melaporkan ukuran dan menampilkan petunjuk penempatan.
  4. Node root Row meminta pengukuran turunan keduanya, Column.
  5. Node Column meminta pengukuran turunan Text pertamanya.
  6. Node Text pertama adalah node daun, sehingga melaporkan ukuran dan menampilkan petunjuk penempatan.
  7. Node Column meminta pengukuran turunan keduanya, Text.
  8. Node Text kedua adalah node daun, sehingga melaporkan ukuran dan menampilkan petunjuk penempatan.
  9. Setelah node Column menyelesaikan pengukuran, pengubahan ukuran, dan menempatkan turunannya, node tersebut dapat menentukan ukuran dan penempatannya sendiri.
  10. Setelah node root Row menyelesaikan pengukuran, pengubahan ukuran, dan menempatkan turunannya, node root tersebut dapat menentukan ukuran dan penempatannya sendiri.

Urutan pengukuran, pengubahan ukuran, dan penempatan di hierarki UI Hasil Penelusuran

Performa

Compose mencapai performa tinggi dengan mengukur turunan hanya satu kali. Pengukuran single-pass baik untuk performa, memungkinkan Compose menangani hierarki UI yang dalam secara efisien. Jika elemen mengukur turunannya dua kali dan turunan itu mengukur setiap turunannya dua kali dan seterusnya, satu upaya untuk membuat tata letak seluruh UI harus melakukan banyak pekerjaan, sehingga sulit untuk menjaga aplikasi tetap berperforma tinggi.

Jika tata letak Anda memerlukan beberapa pengukuran karena alasan tertentu, Compose menawarkan sistem khusus, yaitu pengukuran intrinsik. Anda dapat membaca fitur ini lebih lanjut di Pengukuran intrinsik di tata letak Compose.

Karena pengukuran dan penempatan adalah sub-fase penerusan tata letak yang berbeda, setiap perubahan yang hanya memengaruhi penempatan item, bukan pengukuran, dapat dijalankan secara terpisah.

Pengubah

Pengubah memungkinkan Anda mendekorasi atau meningkatkan composable. Pengubah memungkinkan Anda melakukan hal-hal berikut:

  • Mengubah ukuran, tata letak, perilaku, dan tampilan yang dapat dikomposisi
  • Menambahkan informasi, seperti label aksesibilitas
  • Memproses input pengguna
  • Menambahkan interaksi tingkat tinggi, seperti membuat elemen yang dapat diklik, dapat di-scroll, dapat ditarik, atau dapat di-zoom

Pengubah adalah objek Kotlin standar. Buat pengubah dengan memanggil salah satu fungsi class Modifier. Anda dapat menggabungkan fungsi-fungsi berikut untuk mengomposisikannya:

@Composable
fun ArtistCard(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
        Spacer(Modifier.size(padding))
        Card(elevation = 4.dp) { /*...*/ }
    }
}

Tata letak yang lebih kompleks lagi, menggunakan pengubah untuk mengubah cara mengatur grafik dan area mana yang merespons input pengguna

Dalam kode di atas, perhatikan berbagai fungsi pengubah yang digunakan bersama-sama.

  • clickable membuat composable bereaksi terhadap input pengguna dan menampilkan ripple.
  • padding menempatkan ruang di sekitar elemen.
  • fillMaxWidth membuat fungsi yang dapat dikomposisi mengisi lebar maksimum yang diberikan kepadanya dari induknya.
  • size() menentukan lebar dan tinggi yang dipilih untuk elemen.

Pentingnya urutan pengubah

Urutan fungsi pengubah signifikan. Karena setiap fungsi melakukan perubahan pada Modifier yang ditampilkan oleh fungsi sebelumnya, urutan tersebut akan memengaruhi hasil akhir. Mari kita lihat contohnya:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

Seluruh area, termasuk padding di sekitar tepinya, merespons klik

Pada kode di atas, seluruh area dapat diklik, termasuk padding di sekitarnya, karena pengubah padding telah diterapkan setelah pengubah clickable. Jika urutan pengubah dibalik, ruang yang ditambahkan oleh padding tidak akan merespons input pengguna:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

Padding di sekitar tepi tata letak tidak lagi merespons klik

Pengubah bawaan

Jetpack Compose menyediakan daftar pengubah bawaan untuk membantu Anda mendekorasi atau meningkatkan composable. Beberapa pengubah seperti padding, clickable, dan fillMaxWidth telah diperkenalkan. Berikut daftar pengubah umum lainnya:

ukuran

Secara default, tata letak yang disediakan di Compose menggabungkan turunannya. Namun, Anda dapat menyetel ukuran dengan menggunakan pengubah size:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

Perhatikan bahwa ukuran yang Anda tentukan mungkin tidak diterapkan jika tidak memenuhi batasan yang berasal dari induk tata letak. Jika Anda mengharuskan ukuran composable diperbaiki, terlepas dari batasan yang masuk, gunakan pengubah requiredSize:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

Gambar turunan lebih besar daripada batasan yang berasal dari induknya

Dalam contoh ini, meski induk height disetel ke 100.dp, tinggi Image akan menjadi 150.dp, karena pengubah requiredSize lebih diutamakan.

Jika Anda ingin tata letak turunan mengisi semua tinggi yang tersedia yang diizinkan oleh induk, tambahkan pengubah fillMaxHeight (Compose juga menyediakan fillMaxSize dan fillMaxWidth):

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.fillMaxHeight()
        )
        Column { /*...*/ }
    }
}

Tinggi gambar sama dengan induknya

Jika Anda ingin menambahkan padding di atas dasar pengukuran teks sehingga Anda mencapai jarak tertentu dari bagian atas tata letak ke dasar pengukuran, gunakan pengubah paddingFromBaseline:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(
                text = artist.name,
                modifier = Modifier.paddingFromBaseline(top = 50.dp)
            )
            Text(artist.lastSeenOnline)
        }
    }
}

Teks dengan padding di atasnya

Offset

Untuk menempatkan tata letak relatif ke posisi aslinya, tambahkan pengubah offset dan setel offset dalam sumbu x dan y. Offset bisa positif dan tidak positif. Perbedaan antara padding dan offset adalah menambahkan offset pada composable tidak mengubah pengukurannya:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

Teks digeser ke sisi kanan penampung induknya

Pengubah offset diterapkan secara horizontal sesuai dengan arah tata letak. Dalam konteks kiri-ke-kanan, offset positif menggeser elemen ke kanan, sedangkan dalam konteks kanan-ke-kiri, elemen akan bergeser ke kiri. Jika Anda perlu menyetel offset tanpa mempertimbangkan arah tata letak, lihat pengubah absoluteOffset, ketika nilai offset positif selalu menggeser elemen ke kanan.

Keamanan jenis di Compose

Di Compose, ada pengubah yang hanya berfungsi saat diterapkan ke turunan composable tertentu. Misalnya, jika Anda ingin membuat turunan sebesar Box induk tanpa memengaruhi ukuran Box, gunakan pengubah matchParentSize.

Compose menerapkan keamanan jenis ini melalui cakupan kustom. Misalnya, matchParentSize hanya tersedia di BoxScope. Oleh karena itu, hanya dapat digunakan saat turunan digunakan dalam Box.

Pengubah cakupan memberi tahu induk tentang beberapa informasi turunan yang harus diketahui induk. Ini juga biasa disebut sebagai pengubah data induk. Internalnya berbeda dari pengubah tujuan umum, tetapi dari perspektif penggunaan, perbedaan ini tidak penting.

matchParentSize di Box

Seperti yang disebutkan di atas, jika Anda ingin tata letak turunan berukuran sama dengan Box induk tanpa memengaruhi ukuran Box, gunakan pengubah matchParentSize.

Perhatikan bahwa matchParentSize hanya tersedia dalam cakupan Box, artinya hanya berlaku untuk turunan langsung dari Box composable.

Dalam contoh di bawah ini, Spacer turunan mengambil ukurannya dari Box induknya, yang kemudian mengambil ukurannya dari turunan terbesar, dalam hal ini ArtistCard.

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(Modifier.matchParentSize().background(Color.LightGray))
        ArtistCard()
    }
}

Latar belakang abu-abu yang mengisi penampungnya

Jika fillMaxSize digunakan sebagai pengganti matchParentSize, Spacer akan mengambil semua ruang tersedia yang diizinkan untuk induk, yang pada akhirnya menyebabkan induk memperluas dan mengisi semua ruang yang tersedia.

Latar belakang abu-abu yang memenuhi layar

bobot dalam Baris dan Kolom

Seperti yang telah Anda lihat di bagian sebelumnya tentang Padding dan ukuran, secara default, ukuran composable ditentukan oleh konten yang digabungkannya. Anda dapat menyetel ukuran composable agar fleksibel dalam induknya menggunakan Pengubah weight yang hanya tersedia di RowScope, dan ColumnScope.

Mari kita ambil Row yang berisi dua composable Box. Kotak pertama diberikan dua kali weight kotak kedua, sehingga diberi lebar dua kali lipat. Karena Row selebar 210.dp, Box pertama selebar 140.dp, dan kedua selebar 70.dp:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Image(
            /*...*/
            modifier = Modifier.weight(2f)
        )
        Column(
            modifier = Modifier.weight(1f)
        ) {
            /*...*/
        }
    }
}

Lebar gambar dua kali lebar teks

Tata letak yang dapat di-scroll

Pelajari lebih lanjut tata letak yang dapat di-scroll dalam Dokumentasi gestur Compose.

Untuk daftar dan daftar lambat, lihat Dokumentasi daftar Compose.

Layout responsif

Tata letak harus didesain dengan mempertimbangkan berbagai orientasi layar dan ukuran faktor bentuk. Compose menawarkan beberapa mekanisme untuk memfasilitasi penyesuaian tata letak composable ke berbagai konfigurasi layar.

Batasan

Untuk mengetahui batasan yang berasal dari induk dan mendesain tata letak yang sesuai, Anda dapat menggunakan BoxWithConstraints. Batasan pengukuran dapat ditemukan dalam cakupan lambda konten. Anda dapat menggunakan batasan pengukuran ini untuk membuat tata letak yang berbeda untuk konfigurasi layar yang berbeda:

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}

Tata letak berbasis slot

Compose menyediakan berbagai macam composable berdasarkan Desain Material dengan dependensi androidx.compose.material:material (disertakan saat membuat project Compose di Android Studio) agar proses mem-build UI menjadi lebih mudah. Elemen seperti Drawer, FloatingActionButton, dan TopAppBar semuanya disediakan.

Komponen material banyak menggunakan API slot, yakni sebuah pola yang dibuat Compose untuk menghadirkan lapisan penyesuaian di atas composable. Pendekatan ini membuat komponen lebih fleksibel, karena menerima elemen turunan yang dapat mengonfigurasi sendiri, bukan harus menampilkan setiap parameter konfigurasi turunan. Slot memberikan ruang kosong di UI untuk diisi developer, sesuai keinginan mereka. Misalnya, ini adalah slot yang dapat Anda sesuaikan dalam TopAppBar:

Diagram yang menampilkan slot yang tersedia di panel aplikasi Komponen Material

Komponen biasanya menggunakan lambda komponen content ( content: @Composable () -> Unit). API slot menampilkan beberapa parameter content untuk penggunaan tertentu. Misalnya, TopAppBar memungkinkan Anda menyediakan konten untuk title, navigationIcon, dan actions.

Misalnya, Scaffold memungkinkan Anda menerapkan UI dengan struktur tata letak Desain Material dasar. Scaffold menyediakan slot untuk komponen Material tingkat atas yang paling umum, seperti TopAppBar, BottomAppBar, FloatingActionButton, dan Drawer. Dengan menggunakan Scaffold, sangat mudah untuk memastikan komponen ini diposisikan dengan benar dan bekerja bersama dengan benar.

Contoh aplikasi JetNews, yang menggunakan Scaffold untuk memosisikan beberapa elemen

@Composable
fun HomeScreen(/*...*/) {
    Scaffold(
        drawerContent = { /*...*/ },
        topBar = { /*...*/ },
        content = { /*...*/ }
    )
}