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 dukungan selengkapnya saat Anda mempelajari codelab ini, lihat kode berikut:

Prasyarat

  • Pengalaman dengan sintaksis Kotlin, termasuk lambda

Yang akan Anda lakukan

Dalam codelab ini, Anda akan mempelajari:

  • Apa itu Compose
  • Cara mem-build 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:

87f2753c576d26f2.gif

Yang Anda butuhkan

2. Memulai project Compose baru

Untuk memulai project Compose baru, buka Android Studio Bumblebee dan pilih Start a new Android Studio project seperti yang ditampilkan:

ec53715fe31913e6.jpeg

Jika layar di atas tidak muncul, buka File > New > New Project.

Saat membuat project baru, pilih Empty Compose Activity dari template yang tersedia.

a67ba73a4f06b7ac.png

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

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

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

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

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

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    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/googlecodelabs/android-compose-codelabs

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 dibuat 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, String. Text adalah fungsi composable yang disediakan oleh library.

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

Compose di aplikasi Android

Dengan Compose, Activities 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 dibandingkan 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(color = MaterialTheme.colors.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 atau fungsi Composable yang tidak memiliki parameter 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 DefaultPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

88d6e7a2cfc33ed9.png

Pratinjau mungkin tidak muncul jika Code bcf00530a220eea9.png dipilih. Klik Split aadde7eea0921d0f.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.colors.primary.

@Composable
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.primary) {
        Text (text = "Hello $name!")
    }
}

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

Saat menambahkan kode tersebut ke project, Anda akan melihat tombol Build & Refresh di pojok kanan atas Android Studio. Ketuk tombol tersebut atau build project untuk melihat perubahan baru dalam pratinjau.

1886a2cbfefe7df3.png

Anda dapat melihat perubahan baru dalam pratinjau:

a6cd30458c8829a2.png

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

Kita tidak menentukannya. Komponen Material, seperti androidx.compose.material.Surface, dibuat untuk meningkatkan pengalaman Anda 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.

Misalnya, pengubah padding akan menerapkan jumlah ruang di sekitar elemen yang didekorasinya. Anda dapat membuat pengubah padding dengan Modifier.padding().

Sekarang, tambahkan padding ke Text di layar:

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

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

Klik Build & Refresh untuk melihat perubahan yang baru.

52d5ed8919f277f0.png

Ada banyak pengubah yang dapat digunakan untuk menyelaraskan, menganimasikan, menata letak, memberikan kemampuan klik atau scroll, mengubah, dll. Untuk 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.

Buat Composable bernama MyApp yang menyertakan salam.

@Composable
private fun MyApp() {
    Surface(color = MaterialTheme.colors.background) {
        Greeting("Android")
    }
}

Hal ini memungkinkan Anda menghapus callback onCreate dan pratinjau karena kini Anda dapat menggunakan kembali composable MyApp untuk menghindari duplikasi kode. File MainActivity.kt Anda seharusnya terlihat seperti ini:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.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.codelab.basicstep1.ui.theme.BasicsCodelabTheme

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

@Composable
private fun MyApp() {
    Surface(color = MaterialTheme.colors.background) {
        Greeting("Android")
    }
}

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

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

6. Membuat kolom dan baris

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

fbd450e8eab10338.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:

cf26127d32021cd.png

Perhatikan bahwa Anda mungkin harus memindahkan padding.

Bandingkan hasilnya dengan solusi ini:

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

@Composable
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.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(names: List<String> = listOf("World", "Compose")) {
    Column {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

c366a44b3e99157f.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 DefaultPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

63ac84a794ec7a3d.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.

ecd3370d03f7130e.png

Sekarang bandingkan kode Anda dengan solusinya:

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

@Composable
private fun Greeting(name: String) {
    Surface(
        color = MaterialTheme.colors.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, misalnya, 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 untuk membuat tata letak berikut:

203e0a9946f313cc.png

Button adalah composable yang disediakan oleh paket material 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, menjadikannya fleksibel, secara efektif mendorong elemen lain yang tidak memiliki bobot, 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.material.Button
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
...

@Composable
private fun Greeting(name: String) {

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                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:

e3914108b7082ac0.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 salam, tempat yang logis untuk hal ini adalah dalam composable Greeting. Lihat boolean expanded ini dan cara penggunaannya dalam kode:

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

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                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, 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.

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

Jika menjalankan aplikasi di emulator, Anda dapat melihat bahwa 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.

825dd6d6f98bff05.gif

Kode hingga tahap ini:

@Composable
private fun Greeting(name: String) {
    val expanded = remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                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
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

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

Anda tidak perlu mengingat extraPadding terhadap rekomposisi karena bergantung pada status dan sedang melakukan penghitungan sederhana.

Dan sekarang kita dapat menerapkan pengubah padding baru ke Kolom:

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

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

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Jika menjalankan pada emulator, Anda akan melihat bahwa setiap item dapat diperluas secara independen:

e3914108b7082ac0.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.

8c0da5d9a631ba97.png

Tambahkan kode berikut ke MainActivity.kt:

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment

...

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

    Surface {
        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 "Lanjutkan".

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 salam, lakukan hal berikut:

// Don't copy yet
@Composable
fun MyApp() {
    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:

@Composable
fun MyApp() {
     Greetings()
}

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

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

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

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

Bagaimana cara meneruskan peristiwa? Dengan meneruskan 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() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        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 akan terlihat seperti aplikasi sebenarnya, bagus!

1fd101673cd56005.gif

Kode lengkap sejauh ini:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.layout.wrapContentSize
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.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.BasicsCodelabTheme

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

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        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(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
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

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

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

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

9. Membuat daftar lambat berperforma tinggi

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

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 salam, bahkan salam 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(names: List<String> = List(1000) { "$it" } ) {
    LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

487ca9093596fc6c.gif

10. Mempertahankan status

Aplikasi mengalami masalah: 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:

    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.

63be420fc371e10f.gif

Demo yang menunjukkan perubahan konfigurasi (beralih ke mode gelap) tidak menampilkan orientasi lagi.

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:

50756832c3714a6f.gif

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

Buat extraPadding animasi yang bergantung pada status yang diperluas. Selain itu, mari kita gunakan delegasi properti (kata kunci by):

@Composable
private fun Greeting(name: String) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

Jalankan aplikasi dan coba animasi.

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

@Composable
private fun Greeting(name: String) {

    var expanded by remember { 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 bergantung pada properti fisik (redaman dan kekakuan) untuk membuat animasi menjadi lebih alami. Jalankan aplikasi sekarang untuk mencoba animasi baru:

489e7c08d5c46781.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:

354ddf3f23ebb8e0.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

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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.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.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.BasicsCodelabTheme

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

@Composable
fun MyApp() {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        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(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) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

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

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.kt, Anda akan melihat bahwa BasicsCodelabTheme menggunakan MaterialTheme dalam penerapannya:

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        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()
    }

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

            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.h4)
            }

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 h1-h6, body1,body2, caption, subtitle1, dll. Dalam contoh, Anda menggunakan gaya h4 yang ditentukan dalam tema.

Sekarang build untuk melihat teks dengan gaya baru:

5a2a3b62960cd0e9.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 kesalahan 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:

                Text(
                    text = name,
                    style = MaterialTheme.typography.h4.copy(
                        fontWeight = FontWeight.ExtraBold
                    )
                )

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

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

619daa86b737c945.png

Menyesuaikan tema aplikasi

Anda dapat menemukan semua yang terkait dengan tema saat ini dalam file di dalam folder ui. 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 LightColorPalette = lightColors(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

Jika Anda kembali ke MainActivity.kt dan memuat ulang pratinjau, Anda akan melihat warna baru:

479f2f0e8f19c3e9.png

Namun, Anda belum mengubah warna gelap. Sebelum melakukannya, mari kita siapkan pratinjaunya. Tambahkan anotasi @Preview tambahan ke DefaultPreview dengan UI_MODE_NIGHT_YES:

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

Tindakan ini akan menambahkan pratinjau dalam mode gelap.

8a54a386b258277a.png

Di Theme.kt, tentukan palet untuk warna gelap:

private val DarkColorPalette = darkColors(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

Sekarang, aplikasi kita memiliki tema dan gaya!

19e76f3aa95940af.png

Kode akhir untuk Theme.kt

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable

private val DarkColorPalette = darkColors(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

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

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        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:

87f2753c576d26f2.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 Anda.
implementation "androidx.compose.material:material-icons-extended:$compose_version"
  • 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, 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.

Kode akhir

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.Button
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.codelab.basics.R
import com.codelab.basics.ui.BasicsCodelabTheme

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

@Composable
private fun MyApp() {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

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

@Composable
private fun OnboardingScreen(onContinueClicked: () -> Unit) {
    Surface {
        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(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) {
    Card(
        backgroundColor = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by remember { 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.h4.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 = "DefaultPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

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

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/googlecodelabs/android-compose-codelabs

Atau, Anda dapat mendownload repositori sebagai file Zip:

Apa selanjutnya?

Lihat codelab lain di jalur Compose:

Bacaan lebih lanjut