Dasar-Dasar Jetpack Compose

1. Sebelum memulai

Jetpack Compose adalah toolkit modern yang dirancang untuk menyederhanakan pengembangan UI. Toolkit ini menggabungkan model pemrograman reaktif dengan keringkasan dan kemudahan penggunaan bahasa pemrograman Kotlin. Alat ini sepenuhnya deklaratif, artinya Anda mendeskripsikan UI dengan memanggil serangkaian fungsi yang mengubah data menjadi hierarki UI. Saat data yang mendasari berubah, framework akan otomatis mengeksekusi ulang fungsi ini, yang mengupdate hierarki UI untuk Anda.

Aplikasi Compose terdiri dari fungsi composable - hanya fungsi reguler yang ditandai dengan @Composable, yang dapat memanggil fungsi composable lainnya. Anda hanya perlu menggunakan fungsi ini untuk membuat komponen UI baru. Anotasi memberi tahu Compose untuk menambahkan dukungan khusus ke fungsi untuk mengupdate dan mengelola UI dari waktu ke waktu. Compose memungkinkan Anda menyusun kode menjadi potongan-potongan kecil. Fungsi composable sering disebut dengan "composable".

Dengan membuat composable kecil yang dapat digunakan kembali, Anda dapat dengan mudah mem-build library elemen UI yang digunakan di aplikasi. Masing-masing bertanggung jawab atas satu bagian layar dan dapat diedit secara terpisah.

Untuk mendapatkan dukungan lebih lanjut saat Anda mempelajari codelab ini, lihat video tutorial coding berikut:

Catatan: Video tutorial coding ini menggunakan Material 2 sementara codelab telah diperbarui ke Material 3. Perlu diketahui bahwa akan ada beberapa langkah yang berbeda.

Prasyarat

  • Pengalaman dengan sintaksis Kotlin, termasuk lambda

Yang akan Anda lakukan

Dalam codelab ini, Anda akan mempelajari:

  • Apa itu Compose
  • Cara membangun UI dengan Compose
  • Cara mengelola status dalam fungsi composable
  • Cara membuat daftar berperforma tinggi
  • Cara menambahkan animasi
  • Cara menata gaya dan tema aplikasi

Anda akan mem-build aplikasi dengan layar orientasi dan daftar item animasi yang diperluas:

8d24a786bfe1a8f2.gif

Yang akan Anda butuhkan

2. Memulai project Compose baru

Untuk memulai project Compose baru, buka Android Studio.

Jika baru memulai di jendela Welcome to Android Studio, klik Start a new Android Studio project. Jika sudah membuka project Android Studio, pilih File > New > New Project dari panel menu.

Untuk project baru, pilih Empty Activity dari template yang tersedia.

d12472c6323de500.png

Klik Next lalu konfigurasikan project Anda seperti biasa, beri nama "Basics Codelab". Pastikan Anda memilih minimumSdkVersion setidaknya API level 21, yang merupakan API minimum yang didukung Compose.

Saat memilih template Empty Activity, kode berikut akan dibuat untuk Anda dalam project:

  • Project sudah dikonfigurasi untuk menggunakan Compose.
  • File AndroidManifest.xml dibuat.
  • File build.gradle.kts dan app/build.gradle.kts berisi opsi dan dependensi yang diperlukan untuk Compose.

Setelah menyinkronkan project, buka MainActivity.kt dan periksa kodenya.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting("Android")
    }
}

Di bagian berikutnya, Anda akan melihat fungsi setiap metode dan cara meningkatkannya untuk membuat tata letak yang fleksibel dan dapat digunakan kembali.

Solusi untuk codelab

Anda dapat memperoleh kode untuk solusi codelab ini dari GitHub:

$ git clone https://github.com/android/codelab-android-compose

Atau, Anda dapat mendownload repositori sebagai file Zip:

Anda akan menemukan kode solusi di project BasicsCodelab. Sebaiknya ikuti codelab ini langkah demi langkah sesuai kemampuan Anda sendiri dan lihat solusi jika diperlukan. Selama codelab, Anda akan melihat cuplikan kode yang harus ditambahkan ke project.

3. Memulai Compose

Buka berbagai class dan metode yang terkait dengan Compose yang telah dibuatkan Android Studio untuk Anda.

Fungsi composable

Fungsi composable adalah fungsi reguler yang dianotasi dengan @Composable. Hal ini memungkinkan fungsi Anda memanggil fungsi @Composable lain di dalamnya. Anda dapat melihat cara fungsi Greeting ditandai sebagai @Composable. Fungsi ini akan menghasilkan bagian hierarki UI yang menampilkan input yang diberikan, yaitu String. Text adalah fungsi composable yang disediakan oleh library.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

Compose di aplikasi Android

Dengan Compose, Activity tetap menjadi titik entri ke aplikasi Android. Di project kita, MainActivity diluncurkan saat pengguna membuka aplikasi (seperti yang ditetapkan dalam file AndroidManifest.xml). Anda menggunakan setContent untuk menentukan tata letak, tetapi alih-alih menggunakan file XML seperti yang biasa Anda lakukan di sistem View tradisional, Anda memanggil fungsi Composable di dalamnya.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                  modifier = Modifier.fillMaxSize(),
                  color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme adalah cara menetapkan gaya fungsi Composable. Anda akan melihat lebih banyak tentang hal ini di bagian Tema aplikasi. Untuk melihat bagaimana teks ditampilkan di layar, Anda dapat menjalankan aplikasi di emulator atau perangkat, atau menggunakan pratinjau Android Studio.

Untuk menggunakan pratinjau Android Studio, Anda hanya perlu menandai fungsi Composable yang tidak memiliki parameter atau fungsi dengan parameter default dengan anotasi @Preview dan mem-build project Anda. Anda sudah dapat melihat fungsi Preview Composable di file MainActivity.kt. Anda dapat memiliki beberapa pratinjau di file yang sama dan memberinya nama.

@Preview(showBackground = true, name = "Text preview")
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

fb011e374b98ccff.png

Pratinjau mungkin tidak muncul jika Code eeacd000622ba9b.png dipilih. Klik Split 7093def1e32785b2.png untuk melihat pratinjau.

4. Menyesuaikan UI

Mari mulai dengan menetapkan warna latar belakang yang berbeda untuk Greeting. Anda dapat melakukannya dengan menggabungkan composable Text dengan Surface. Surface membutuhkan warna, jadi gunakan MaterialTheme.colorScheme.primary.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}

Komponen bertingkat di dalam Surface akan digambar di atas warna latar belakang tersebut.

Anda dapat melihat perubahan baru dalam pratinjau:

c88121ec49bde8c7.png

Anda mungkin melewatkan detail penting: teksnya kini berwarna putih. Kapan kita menentukannya?

Kita tidak menentukannya. Komponen Material, seperti androidx.compose.material3.Surface, dibuat untuk membuat pengalaman Anda jadi lebih baik dengan menangani fitur umum yang mungkin Anda inginkan di aplikasi, seperti memilih warna teks yang sesuai. Kita katakan bahwa Material tidak fleksibel karena memberikan default dan pola bagus yang umum untuk sebagian besar aplikasi. Komponen Material di Compose di-build di atas komponen dasar lainnya (di androidx.compose.foundation), yang juga dapat diakses dari komponen aplikasi Anda jika Anda memerlukan lebih banyak fleksibilitas.

Dalam hal ini, Surface memahami bahwa, saat latar belakang ditetapkan ke warna primary, semua teks di atasnya harus menggunakan warna onPrimary, yang juga ditentukan di tema. Anda dapat mempelajari hal ini lebih lanjut di bagian Tema aplikasi.

Pengubah

Sebagian besar elemen UI Compose seperti Surface dan Text menerima parameter modifier opsional. Pengubah memberi tahu elemen UI cara membuat tata letak, menampilkan, atau berperilaku dalam tata letak induknya. Anda mungkin sudah melihat bahwa composable Greeting sudah memiliki pengubah default, yang kemudian diteruskan ke Text.

Misalnya, pengubah padding akan menerapkan jumlah ruang di sekitar elemen yang didekorasinya. Anda dapat membuat pengubah padding dengan Modifier.padding(). Anda juga dapat menambahkan beberapa pengubah dengan merangkainya sehingga dalam kasus ini, kita dapat menambahkan pengubah padding ke pengubah default: modifier.padding(24.dp).

Sekarang, tambahkan padding ke Text di layar:

import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

ef14f7c54ae7edf.png

Ada banyak pengubah yang dapat digunakan untuk menyelaraskan, menganimasikan, menata letak, memberikan kemampuan klik atau scroll, mengubah, dll. Untuk mengetahui daftar lengkap, lihat Daftar Pengubah Compose. Anda akan menggunakannya di langkah-langkah berikutnya.

5. Menggunakan kembali composable

Makin banyak komponen yang ditambahkan ke UI, makin banyak level bertingkat yang Anda buat. Hal ini dapat memengaruhi keterbacaan jika fungsi menjadi sangat besar. Dengan membuat komponen kecil yang dapat digunakan kembali, Anda dapat dengan mudah mem-build library elemen UI yang digunakan di aplikasi. Masing-masing bertanggung jawab atas satu bagian kecil layar dan dapat diedit secara terpisah.

Sebagai praktik terbaik, fungsi harus menyertakan parameter Pengubah yang ditetapkan sebagai Pengubah kosong secara default. Teruskan pengubah ini ke composable pertama yang Anda panggil dalam fungsi. Dengan cara ini, situs panggilan dapat menyesuaikan petunjuk tata letak dan perilaku dari luar fungsi composable Anda.

Buat Composable bernama MyApp yang menyertakan kata sambutan.

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

Cara ini memungkinkan Anda menghapus callback onCreate dan pratinjau karena kini Anda dapat menggunakan kembali composable MyApp untuk menghindari duplikasi kode.

Dalam pratinjau, panggil MyApp lalu hapus nama pratinjau.

File MainActivity.kt Anda seharusnya terlihat seperti ini:

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

6. Membuat kolom dan baris

Tiga elemen tata letak standar dan dasar di Compose adalah Column, Row, dan Box.

518dbfad23ee1b05.png

Tiga elemen tersebut adalah fungsi Composable yang menggunakan konten Composable sehingga Anda dapat menempatkan item di dalamnya. Misalnya, setiap turunan di dalam Column akan ditempatkan secara vertikal.

// Don't copy over
Column {
    Text("First row")
    Text("Second row")
}

Sekarang, coba ubah Greeting agar menampilkan kolom dengan dua elemen teks seperti dalam contoh ini:

bf27ee688c3231df.png

Perhatikan bahwa Anda mungkin harus memindahkan padding.

Bandingkan hasilnya dengan solusi ini:

import androidx.compose.foundation.layout.Column
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Column(modifier = modifier.padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Compose dan Kotlin

Fungsi composable dapat digunakan seperti fungsi lainnya di Kotlin. Fungsi ini membuat versi UI sangat canggih karena Anda dapat menambahkan pernyataan untuk memengaruhi cara UI ditampilkan.

Misalnya, Anda dapat menggunakan loop for untuk menambahkan elemen ke Column:

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

a7ba2a8cb7a7d79d.png

Anda belum menetapkan dimensi atau menambahkan batasan ke ukuran composable sehingga setiap baris memerlukan ruang minimum yang dapat diambil dan pratinjau melakukan hal yang sama. Mari kita ubah pratinjau untuk mengemulasi lebar umum ponsel kecil, 320 dp. Tambahkan parameter widthDp ke anotasi @Preview seperti berikut:

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

a5d5f6cdbdd918a2.png

Pengubah digunakan secara luas di Compose, jadi mari kita berlatih dengan latihan yang lebih canggih: Coba buat kembali tata letak berikut menggunakan pengubah fillMaxWidth dan padding.

a9599061cf49a214.png

Sekarang bandingkan kode Anda dengan solusinya:

import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Perhatikan bahwa:

  • Pengubah dapat memiliki overload, sehingga Anda dapat menentukan cara yang berbeda untuk membuat padding.
  • Untuk menambahkan beberapa pengubah ke elemen, Anda cukup menggabungkannya.

Ada beberapa cara untuk mencapai hasil ini, jadi jika kode Anda tidak cocok dengan cuplikan ini, bukan berarti kode Anda salah. Namun, salin dan tempel kode ini untuk melanjutkan codelab.

Menambahkan tombol

Pada langkah berikutnya, Anda akan menambahkan elemen yang dapat diklik yang memperluas Greeting, sehingga kita perlu menambahkan tombol tersebut terlebih dahulu. Tujuannya adalah membuat tata letak berikut:

ff2d8c3c1349a891.png

Button adalah composable yang disediakan oleh paket material3 yang menggunakan composable sebagai argumen terakhir. Karena lambda akhir dapat dipindahkan di luar tanda kurung, Anda dapat menambahkan konten apa pun ke tombol sebagai turunan. Misalnya, Text:

// Don't copy yet
Button(
    onClick = { } // You'll learn about this callback later
) {
    Text("Show less")
}

Untuk melakukannya, Anda perlu mempelajari cara menempatkan composable di akhir baris. Tidak ada pengubah alignEnd, jadi sebagai gantinya, Anda memberikan beberapa weight ke composable di awal. Pengubah weight membuat elemen mengisi semua ruang yang tersedia, yang menjadikannya fleksibel, sehingga secara efektif mendorong elemen lain yang tidak memiliki bobot, yaitu yang tidak fleksibel. Perubahan ini juga menjadikan pengubah fillMaxWidth redundan.

Sekarang, coba tambahkan tombol dan tempatkan seperti yang ditunjukkan pada gambar sebelumnya.

Lihat solusinya di sini:

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ElevatedButton
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { /* TODO */ }
            ) {
                Text("Show more")
            }
        }
    }
}

7. Status dalam Compose

Di bagian ini, Anda akan menambahkan beberapa interaksi ke layar. Sejauh ini Anda telah membuat tata letak statis, tetapi sekarang Anda akan membuatnya bereaksi terhadap perubahan pengguna untuk mencapai ini:

6675d41779cac69.gif

Sebelum masuk ke cara membuat tombol yang dapat diklik dan cara mengubah ukuran item, Anda perlu menyimpan beberapa nilai di suatu tempat yang menunjukkan apakah setiap item diperluas atau tidak—status item. Karena kita harus memiliki salah satu nilai ini per kata sambutan, tempat yang logis untuk hal ini adalah dalam composable Greeting. Lihat boolean expanded ini dan cara penggunaannya dalam kode:

// Don't copy over
@Composable
fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

Perhatikan bahwa kita juga menambahkan tindakan onClick dan teks tombol dinamis. Selengkapnya mengenai hal itu akan dibahas nanti.

Namun, hal ini tidak akan berfungsi seperti yang diharapkan. Menetapkan nilai yang berbeda untuk variabel expanded tidak akan membuat Compose mendeteksinya sebagai perubahan status sehingga tidak akan ada yang terjadi.

Alasan mengubah variabel ini tidak memicu rekomposisi adalah karena variabel ini tidak dilacak oleh Compose. Selain itu, setiap kali Greeting dipanggil, variabel akan direset ke salah.

Untuk menambahkan status internal ke composable, Anda dapat menggunakan fungsi mutableStateOf yang membuat Compose merekomposisi fungsi yang membaca State.

import androidx.compose.runtime.mutableStateOf
// ...

// Don't copy over
@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) // Don't do this!
}

Namun, Anda tidak bisa hanya menetapkan mutableStateOf ke variabel di dalam composable. Seperti yang dijelaskan sebelumnya, rekomposisi dapat terjadi kapan saja yang akan kembali memanggil composable, yang mereset status ke status baru yang dapat diubah dengan nilai false.

Untuk mempertahankan status di seluruh rekomposisi, ingat status yang dapat diubah menggunakan remember.

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// ...

@Composable
fun Greeting(...) {
    val expanded = remember { mutableStateOf(false) }
    // ...
}

remember digunakan untuk menjaga dari rekomposisi, sehingga status tidak direset.

Perhatikan bahwa jika Anda memanggil composable yang sama dari bagian layar yang berbeda, Anda akan membuat elemen UI yang berbeda, masing-masing dengan versi statusnya sendiri. Anda dapat menganggap status internal sebagai variabel pribadi dalam class.

Fungsi composable akan secara otomatis "berlangganan" ke status ini. Jika status berubah, composable yang membaca kolom ini akan direkomposisi untuk menampilkan update.

Mengubah status dan bereaksi terhadap perubahan status

Untuk mengubah status, Anda mungkin telah melihat bahwa Button memiliki parameter yang disebut onClick, tetapi tidak memerlukan nilai, parameter ini memerlukan fungsi.

Anda dapat menentukan tindakan yang akan diambil saat diklik dengan menetapkan ekspresi lambda ke tindakan tersebut. Misalnya, mari ubah nilai status yang diperluas, dan tampilkan teks berbeda bergantung pada nilai.

ElevatedButton(
    onClick = { expanded.value = !expanded.value },
) {
   Text(if (expanded.value) "Show less" else "Show more")
}

Jalankan aplikasi dalam mode interaktif untuk melihat perilakunya.

374998ad358bf8d6.png

Saat tombol diklik, expanded akan beralih dan memicu rekomposisi teks di dalam tombol. Setiap Greeting mempertahankan statusnya sendiri yang diperluas karena termasuk dalam elemen UI yang berbeda.

93d839b53b7d9bea.gif

Kode hingga tahap ini:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Memperluas item

Sekarang, mari kita memperluas item saat diminta. Tambahkan variabel tambahan yang bergantung pada status:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
// ...

Anda tidak perlu mengingat extraPadding terhadap rekomposisi karena sedang melakukan penghitungan sederhana.

Selain itu, sekarang kita dapat menerapkan pengubah padding baru ke Kolom:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    val extraPadding = if (expanded.value) 48.dp else 0.dp
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                    .weight(1f)
                    .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Jika menjalankan di emulator atau dalam mode interaktif, Anda akan melihat bahwa setiap item dapat diperluas secara terpisah:

6675d41779cac69.gif

8. Pengangkatan status

Dalam fungsi Composable, status yang dibaca atau diubah oleh beberapa fungsi harus berada di ancestor yang umum—proses ini disebut pengangkatan status. Mengangkat berarti meningkatkan atau mengembangkan.

Membuat status dapat diangkat akan menghindari status duplikat dan memperkenalkan bug, membantu menggunakan kembali composable, dan membuat composable jauh lebih mudah untuk diuji. Sebaliknya, status yang tidak perlu dikontrol oleh induk composable tidak boleh diangkat. Sumber kebenaran adalah milik siapa pun yang membuat dan mengontrol status tersebut.

Misalnya, mari kita membuat layar orientasi untuk aplikasi.

5d5f44508fcfa779.png

Tambahkan kode berikut ke MainActivity.kt:

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
// ...

@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    // TODO: This state should be hoisted
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = { shouldShowOnboarding = false }
        ) {
            Text("Continue")
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen()
    }
}

Kode ini berisi banyak fitur baru:

  • Anda telah menambahkan composable baru yang disebut OnboardingScreen dan juga pratinjau baru. Saat mem-build project, Anda akan melihat beberapa pratinjau sekaligus. Kami juga menambahkan tinggi tetap untuk memverifikasi bahwa konten disejajarkan dengan benar.
  • Column dapat dikonfigurasi untuk menampilkan kontennya di tengah layar.
  • shouldShowOnboarding menggunakan kata kunci by, bukan =. Ini adalah delegasi properti agar Anda tidak perlu mengetik .value setiap saat.
  • Saat tombol diklik, shouldShowOnboarding ditetapkan ke false, tetapi Anda belum membaca status dari mana pun.

Sekarang kita dapat menambahkan layar orientasi baru ini ke aplikasi. Kita ingin menampilkannya saat peluncuran, lalu menyembunyikannya saat pengguna menekan "Continue".

Di Compose, Anda tidak menyembunyikan elemen UI. Sebagai gantinya, Anda tidak menambahkannya ke komposisi, sehingga elemen tidak ditambahkan ke hierarki UI yang dihasilkan Compose. Anda melakukannya dengan logika Kotlin bersyarat yang sederhana. Misalnya, untuk menampilkan layar orientasi atau daftar kata sambutan, lakukan hal berikut:

// Don't copy yet
@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(modifier) {
        if (shouldShowOnboarding) { // Where does this come from?
            OnboardingScreen()
        } else {
            Greetings()
        }
    }
}

Namun, kita tidak memiliki akses ke shouldShowOnboarding. Jelas bahwa kita perlu membagikan status yang dibuat di OnboardingScreen dengan composable MyApp.

Daripada membagikan nilai status kepada induknya, kita mengangkat status. Kita hanya memindahkannya ke ancestor umum yang perlu mengaksesnya.

Pertama, pindahkan konten MyApp ke composable baru yang disebut Greetings. Sesuaikan juga pratinjau untuk memanggil metode Greetings:

@Composable
fun MyApp(modifier: Modifier = Modifier) {
     Greetings()
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingsPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

Tambahkan pratinjau untuk composable MyApp tingkat atas baru agar kami dapat menguji perilakunya:

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

Sekarang tambahkan logika untuk menampilkan layar yang berbeda di MyApp, dan angkat statusnya.

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(/* TODO */)
        } else {
            Greetings()
        }
    }
}

Kita juga perlu membagikan shouldShowOnboarding ke layar orientasi, tetapi kita tidak akan meneruskannya secara langsung. Sebaiknya kita mengizinkan OnboardingScreen memberi tahu kita saat pengguna mengklik tombol Continue, bukan membiarkannya mengubah status.

Bagaimana cara mengabaikan peristiwa? Dengan menurunkan callback. Callback adalah fungsi yang diteruskan sebagai argumen ke fungsi lainnya dan dijalankan saat peristiwa terjadi.

Coba tambahkan parameter fungsi ke layar orientasi yang ditentukan sebagai onContinueClicked: () -> Unit sehingga Anda dapat mengubah status dari MyApp.

Solusi:

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier
                .padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

Dengan meneruskan fungsi, bukan status, ke OnboardingScreen, kita membuat composable ini dapat digunakan kembali dan melindungi status agar tidak diubah oleh composable lain. Secara umum, semuanya tetap sederhana. Contoh yang bagus adalah bagaimana pratinjau orientasi perlu dimodifikasi untuk memanggil OnboardingScreen sekarang:

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {}) // Do nothing on click.
    }
}

Menetapkan onContinueClicked ke ekspresi lambda kosong berarti "tidak melakukan apa pun", yang sangat cocok untuk pratinjau.

Ini makin tampak seperti aplikasi sebenarnya, bagus!

25915eb273a7ef49.gif

Dalam composable MyApp, kita menggunakan delegasi properti by untuk pertama kalinya agar tidak selalu menggunakan nilai. Mari kita gunakan by, bukan =, juga di composable Greeting untuk properti expanded. Pastikan Anda mengubah expanded dari val menjadi var.

Kode lengkap sejauh ini:

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding = if (expanded) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

9. Membuat daftar lambat berperforma tinggi

Sekarang mari kita buat daftar nama yang lebih realistis. Sejauh ini Anda telah menampilkan dua sambutan dalam Column. Namun, apakah kode tersebut dapat menangani ribuan sambutan?

Ubah nilai daftar default dalam parameter Greetings untuk menggunakan konstruktor daftar lain yang memungkinkan untuk menetapkan ukuran daftar dan mengisinya dengan nilai yang terdapat dalam lambda-nya (di sini $it mewakili indeks daftar):

names: List<String> = List(1000) { "$it" }

Tindakan ini akan menghasilkan 1.000 sambutan, bahkan sambutan yang tidak sesuai dengan layar. Tentu saja ini bukan performa yang bagus. Anda dapat mencoba menjalankannya di emulator (peringatan: kode ini mungkin menghentikan emulator).

Untuk menampilkan kolom yang dapat di-scroll, kita menggunakan LazyColumn. LazyColumn hanya merender item yang terlihat di layar, sehingga memungkinkan peningkatan performa saat merender daftar besar.

Dalam penggunaan dasarnya, LazyColumn API menyediakan elemen items dalam cakupannya, tempat logika rendering setiap item ditulis:

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
// ...

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

284f925eb984fb56.gif

10. Mempertahankan status

Aplikasi kita memiliki dua masalah:

Mempertahankan status layar orientasi

Jika Anda menjalankan aplikasi di perangkat, mengklik tombol, lalu melakukan rotasi, layar orientasi akan ditampilkan lagi. Fungsi remember hanya berfungsi selama composable disimpan di Komposisi. Saat Anda melakukan rotasi, seluruh aktivitas dimulai ulang, sehingga semua status hilang. Hal ini juga terjadi dengan perubahan konfigurasi dan penghentian proses.

Anda dapat menggunakan rememberSaveable, bukan remember. Setiap perubahan konfigurasi status yang masih ada (seperti rotasi) dan penghentian proses akan disimpan.

Sekarang, ganti penggunaan remember di shouldShowOnboarding dengan rememberSaveable:

    import androidx.compose.runtime.saveable.rememberSaveable
    // ...

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

Jalankan, rotasikan, ubah ke mode gelap, atau hentikan prosesnya. Layar orientasi tidak ditampilkan kecuali Anda sebelumnya telah keluar dari aplikasi.

Mempertahankan status item daftar yang diperluas

Jika Anda memperluas item daftar lalu men-scroll daftar hingga item tidak terlihat, atau memutar perangkat lalu kembali ke item yang diperluas, Anda akan melihat bahwa item tersebut kini kembali ke keadaan awal.

Solusi untuk masalah ini adalah dengan menggunakan rememberSaveable untuk status diperluas juga:

   var expanded by rememberSaveable { mutableStateOf(false) }

Dengan sekitar 120 baris kode sejauh ini, Anda dapat menampilkan daftar scroll item yang panjang dan berperforma tinggi yang masing-masing menyimpan statusnya sendiri. Selain itu, seperti yang dapat Anda lihat, aplikasi Anda memiliki mode gelap yang benar-benar sempurna tanpa baris kode tambahan. Anda akan mempelajari tema nanti.

11. Menganimasikan daftar Anda

Di Compose, ada beberapa cara untuk menganimasikan UI Anda: dari API tingkat tinggi untuk animasi sederhana hingga metode tingkat rendah untuk kontrol penuh dan transisi yang kompleks. Anda dapat membacanya di dokumentasi.

Di bagian ini, Anda akan menggunakan salah satu API tingkat rendah, tetapi jangan khawatir, API ini juga bisa menjadi sangat sederhana. Mari kita animasikan perubahan ukuran yang sudah kita implementasikan:

9efa14ce118d3835.gif

Untuk ini, Anda akan menggunakan composable animateDpAsState. Metode ini menampilkan objek Status yang value-nya akan terus diperbarui oleh animasi hingga selesai. "Nilai target" yang jenisnya adalah Dp akan digunakan.

Buat extraPadding animasi yang bergantung pada status yang diperluas.

import androidx.compose.animation.core.animateDpAsState

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

Jalankan aplikasi dan coba animasinya.

animateDpAsState menggunakan parameter animationSpec opsional yang memungkinkan Anda menyesuaikan animasi. Mari kita lakukan hal yang lebih menyenangkan seperti menambahkan animasi berbasis pegas:

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Surface(
    // ...
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))

    // ...

    )
}

Perhatikan bahwa kita juga memastikan padding tidak pernah negatif, jika tidak, padding dapat membuat aplikasi error. Ini memperkenalkan bug animasi halus yang akan kita perbaiki nanti dalam Sentuhan akhir.

Spesifikasi spring tidak menggunakan parameter terkait waktu. Namun, spesifikasi tersebut bergantung pada properti fisik (redaman dan kekakuan) untuk membuat animasi menjadi lebih alami. Jalankan aplikasi sekarang untuk mencoba animasi baru:

9efa14ce118d3835.gif

Setiap animasi yang dibuat dengan animate*AsState dapat terganggu. Artinya, jika nilai target berubah di tengah animasi, animate*AsState akan memulai ulang animasi dan menunjuk ke nilai baru. Gangguan terlihat sangat alami dengan animasi berbasis pegas:

d5dbf92de69db775.gif

Jika Anda ingin mempelajari berbagai jenis animasi, coba parameter yang berbeda untuk spring, spesifikasi yang berbeda (tween, repeatable), dan fungsi yang berbeda: animateColorAsState atau jenis animation API yang berbeda.

Kode lengkap untuk bagian ini

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

12. Menata gaya dan tema aplikasi

Hingga saat ini Anda belum menata gaya pada composable mana pun, tetapi Anda mendapatkan default yang memadai, termasuk dukungan mode gelap. Mari kita lihat apa itu BasicsCodelabTheme dan MaterialTheme.

Jika membuka file ui/theme/Theme.kt, Anda akan melihat bahwa BasicsCodelabTheme menggunakan MaterialTheme dalam penerapannya:

// Do not copy
@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // ...

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

MaterialTheme adalah fungsi composable yang mencerminkan prinsip gaya dari Spesifikasi desain material. Informasi gaya tersebut menurun ke komponen yang berada dalam content-nya, yang dapat membaca informasi untuk menyesuaikan gayanya sendiri. Di UI, Anda sudah menggunakan BasicsCodelabTheme sebagai berikut:

    BasicsCodelabTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }

Karena BasicsCodelabTheme menggabungkan MaterialTheme secara internal, gaya MyApp disesuaikan dengan properti yang ditentukan dalam tema. Dari composable turunan, Anda dapat mengambil tiga properti MaterialTheme: colorScheme, typography, dan shapes. Gunakan properti tersebut untuk menetapkan gaya header salah satu Text Anda:

            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.headlineMedium)
            }

Composable Text pada contoh di atas menetapkan TextStyle baru. Anda dapat membuat TextStyle Anda sendiri, atau mengambil gaya yang ditentukan tema menggunakan MaterialTheme.typography, mana pun yang lebih disukai. Konstruksi ini memberi Anda akses ke gaya teks yang ditentukan Material, seperti displayLarge, headlineMedium, titleSmall, bodyLarge, labelMedium, dll. Dalam contoh, Anda menggunakan gaya headlineMedium yang ditentukan dalam tema.

Sekarang, bangun untuk melihat teks dengan gaya baru:

673955c38b076f1c.png

Secara umum, jauh lebih baik untuk mempertahankan warna, bentuk, dan gaya font Anda di dalam MaterialTheme. Misalnya, mode gelap akan sulit diterapkan jika Anda melakukan hard code warna dan akan memerlukan banyak pekerjaan yang rentan error untuk diperbaiki.

Namun, terkadang Anda harus sedikit menyimpang dari pilihan warna dan gaya font. Dalam situasi tersebut, lebih baik warna atau gaya didasarkan pada warna atau gaya yang sudah ada.

Untuk melakukannya, Anda dapat mengubah gaya yang telah ditentukan menggunakan fungsi copy. Buat angka lebih tebal:

import androidx.compose.ui.text.font.FontWeight
// ...
Text(
    text = name,
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)

Dengan cara ini, jika harus mengubah jenis font atau atribut headlineMedium lainnya, Anda tidak perlu khawatir dengan penyimpangan kecil.

Sekarang, hasil yang akan tampak di jendela pratinjau adalah sebagai berikut:

b33493882bda9419.png

Menyiapkan pratinjau mode gelap

Saat ini, pratinjau hanya menampilkan tampilan aplikasi dalam mode terang. Tambahkan anotasi @Preview tambahan ke GreetingPreview dengan UI_MODE_NIGHT_YES:

import android.content.res.Configuration.UI_MODE_NIGHT_YES

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

Tindakan ini akan menambahkan pratinjau dalam mode gelap.

2c94dc7775d80166.png

Menyesuaikan tema aplikasi

Anda dapat menemukan semua yang terkait dengan tema saat ini dalam file di dalam folder ui/theme. Misalnya, warna default yang telah kita gunakan sejauh ini ditentukan di Color.kt.

Mari kita mulai dengan menentukan warna baru. Tambahkan ini ke Color.kt:

val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)

Sekarang, tetapkan ke palet MaterialTheme di Theme.kt:

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

Jika Anda kembali ke MainActivity.kt dan memuat ulang pratinjau, warna pratinjau tidak akan benar-benar berubah. Hal ini karena secara default, Pratinjau akan menggunakan warna dinamis. Anda dapat melihat logika untuk menambahkan pewarnaan dinamis di Theme.kt menggunakan parameter boolean dynamicColor.

Untuk melihat versi non-adaptif skema warna, jalankan aplikasi di perangkat dengan API level yang lebih rendah dari 31 (sesuai dengan Android S, tempat warna adaptif diperkenalkan). Anda akan melihat warna baru:

493d754584574e91.png

Di Theme.kt, tentukan palet untuk warna gelap:

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

Saat sekarang menjalankan aplikasi, kita akan melihat cara kerja warna gelap:

84d2a903ffa6d8df.png

Kode akhir untuk Theme.kt

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

13. Sentuhan akhir

Pada langkah ini, Anda akan menerapkan hal yang sudah diketahui dan mempelajari konsep baru hanya dengan beberapa petunjuk. Anda akan membuat ini:

8d24a786bfe1a8f2.gif

Mengganti tombol dengan ikon

  • Gunakan composable IconButton bersama dengan Icon turunan.
  • Gunakan Icons.Filled.ExpandLess dan Icons.Filled.ExpandMore, yang tersedia di artefak material-icons-extended. Tambahkan baris berikut ke dependensi di file app/build.gradle.kts Anda.
implementation("androidx.compose.material:material-icons-extended")
  • Ubah padding untuk memperbaiki penyejajaran.
  • Tambahkan deskripsi konten untuk aksesibilitas (lihat "Menggunakan resource string" di bawah).

Menggunakan resource string

Deskripsi konten untuk "Tampilkan lebih banyak" dan "tampilkan lebih sedikit" harus ada dan Anda dapat menambahkannya dengan pernyataan if sederhana:

contentDescription = if (expanded) "Show less" else "Show more"

Namun, string hard code adalah praktik yang buruk dan Anda harus mendapatkannya dari file strings.xml.

Anda bisa menggunakan "Extract string resource" di setiap string, yang tersedia di "Context Actions" di Android Studio untuk melakukannya secara otomatis.

Atau, buka app/src/res/values/strings.xml dan tambahkan resource berikut:

<string name="show_less">Show less</string>
<string name="show_more">Show more</string>

Menampilkan lebih banyak

Teks "Composem ipsum" muncul dan menghilang, yang memicu perubahan ukuran setiap kartu.

  • Tambahkan Text baru ke Kolom di dalam Greeting yang ditampilkan saat item diperluas.
  • Hapus extraPadding dan terapkan pengubah animateContentSize ke Row. Hal ini akan mengotomatiskan proses pembuatan animasi, yang akan sulit dilakukan secara manual. Selain itu, tindakan ini juga menghilangkan kebutuhan akan coerceAtLeast.

Menambahkan elevasi dan bentuk

  • Anda dapat menggunakan pengubah shadow bersama dengan pengubah clip untuk mendapatkan tampilan kartu. Namun, ada composable Material yang melakukan hal tersebut: Card. Anda dapat mengubah warna Card dengan memanggil CardDefaults.cardColors dan mengganti warna yang ingin diubah.

Kode akhir

package com.example.basicscodelab

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier, color = MaterialTheme.colorScheme.background) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by rememberSaveable { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .padding(12.dp)
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(12.dp)
        ) {
            Text(text = "Hello, ")
            Text(
                text = name, style = MaterialTheme.typography.headlineMedium.copy(
                    fontWeight = FontWeight.ExtraBold
                )
            )
            if (expanded) {
                Text(
                    text = ("Composem ipsum color sit lazy, " +
                        "padding theme elit, sed do bouncy. ").repeat(4),
                )
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }
            )
        }
    }
}

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

14. Selamat

Selamat! Anda telah mempelajari dasar-dasar Compose!

Solusi untuk codelab

Anda dapat memperoleh kode untuk solusi codelab ini dari GitHub:

$ git clone https://github.com/android/codelab-android-compose

Atau, Anda dapat mendownload repositori sebagai file Zip:

Apa selanjutnya?

Lihat codelab lain di jalur Compose:

Bacaan lebih lanjut