Data yang diberi cakupan lokal dengan CompositionLocal

CompositionLocal adalah alat untuk meneruskan data melalui Komposisi secara implisit. Di halaman ini, Anda akan mempelajari definisi CompositionLocal lebih mendetail, cara membuat CompositionLocal Anda sendiri, dan mengetahui apakah CompositionLocal merupakan solusi yang baik untuk kasus penggunaan Anda.

Memperkenalkan CompositionLocal

Biasanya di Compose, data mengalir ke bawah melalui hierarki UI yang berfungsi sebagai parameter untuk setiap fungsi composable. Ini mengakibatkan dependensi composable bersifat eksplisit. Namun, cara ini dapat cukup rumit untuk data yang sangat sering dan banyak digunakan, seperti warna atau gaya jenis. Lihat contoh berikut:

@Composable
fun MyApp() {
    // Theme information tends to be defined near the root of the application
    val colors = colors()
}

// Some composable deep in the hierarchy
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        color = colors.onPrimary // ← need to access colors here
    )
}

Agar tidak perlu meneruskan warna sebagai dependensi parameter eksplisit ke sebagian besar composable, Compose menawarkan CompositionLocal yang memungkinkan Anda membuat objek bernama dengan cakupan hierarki yang dapat digunakan sebagai cara implisit untuk memiliki aliran data melalui hierarki UI.

Elemen CompositionLocal biasanya diberi nilai di node tertentu dalam hierarki UI. Nilai tersebut dapat digunakan oleh turunan composable-nya tanpa mendeklarasikan CompositionLocal sebagai parameter dalam fungsi composable.

CompositionLocal adalah item yang digunakan tema Material di balik layar. MaterialTheme adalah objek yang menyediakan tiga instance CompositionLocal: colorScheme, typography, dan shapes, yang memungkinkan Anda mengambilnya nanti di bagian turunan Komposisi. Secara khusus, ini adalah properti LocalColorScheme, LocalShapes, dan LocalTypography yang dapat Anda akses melalui atribut MaterialTheme colorScheme, shapes, dan typography.

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colorScheme, typography, and shapes are available
        // in MaterialTheme's content lambda.

        // ... content here ...
    }
}

// Some composable deep in the hierarchy of MaterialTheme
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // `primary` is obtained from MaterialTheme's
        // LocalColors CompositionLocal
        color = MaterialTheme.colorScheme.primary
    )
}

Instance CompositionLocal diberi cakupan untuk bagian dari Komposisi sehingga Anda dapat memberikan nilai yang berbeda di berbagai pohon hierarki. Nilai current dari CompositionLocal setara dengan nilai terdekat yang diberikan oleh ancestor di bagian Komposisi tersebut.

Untuk memberikan nilai baru ke CompositionLocal, gunakan CompositionLocalProvider dan fungsi infiks provides-nya yang menghubungkan kunci CompositionLocal ke value. Lambda content dari CompositionLocalProvider akan mendapatkan nilai yang diberikan saat mengakses properti current dari CompositionLocal. Jika nilai baru diberikan, Compose akan merekomposisi bagian Komposisi yang membaca CompositionLocal.

Contohnya, LocalContentColor CompositionLocal berisi warna konten pilihan yang digunakan untuk teks dan ikonografi untuk memastikan warna tersebut kontras dengan warna latar belakang saat ini. Pada contoh berikut, CompositionLocalProvider digunakan untuk memberikan nilai yang berbeda untuk berbagai bagian Komposisi.

@Composable
fun CompositionLocalExample() {
    MaterialTheme {
        // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default
        // This is to automatically make text and other content contrast to the background
        // correctly.
        Surface {
            Column {
                Text("Uses Surface's provided content color")
                CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
                    Text("Primary color provided by LocalContentColor")
                    Text("This Text also uses primary as textColor")
                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
                        DescendantExample()
                    }
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the error color now")
}

Gambar 1. Pratinjau composable CompositionLocalExample.

Dalam contoh terakhir, instance CompositionLocal digunakan secara internal oleh composable Material. Untuk mengakses nilai CompositionLocal saat ini, gunakan properti current-nya. Pada contoh berikut, nilai Context saat ini dari LocalContext CompositionLocal yang umumnya digunakan di aplikasi Android digunakan untuk memformat teks:

@Composable
fun FruitText(fruitSize: Int) {
    // Get `resources` from the current value of LocalContext
    val resources = LocalContext.current.resources
    val fruitText = remember(resources, fruitSize) {
        resources.getQuantityString(R.plurals.fruit_title, fruitSize)
    }
    Text(text = fruitText)
}

Membuat CompositionLocal Anda sendiri

CompositionLocal adalah alat untuk meneruskan data secara implisit melalui Komposisi.

Sinyal penting lainnya untuk menggunakan CompositionLocal adalah ketika parameter memotong silang dan lapisan perantara penerapan tidak boleh mengetahuinya, karena jika lapisan perantara tersebut mengetahuinya, lapisan ini akan membatasi utilitas dari composable. Misalnya, membuat kueri untuk izin Android akan diberikan oleh CompositionLocal di balik layar. Composable pemilih media dapat menambahkan fungsi baru untuk mengakses konten yang dilindungi oleh izin di perangkat tanpa mengubah API-nya dan tidak mengharuskan pemanggil pemilih media mengetahui konteks tambahan ini yang digunakan dari lingkungan.

Namun, CompositionLocal tidak selalu merupakan solusi terbaik. Kami tidak menyarankan CompositionLocal untuk digunakan secara berlebihan karena memiliki beberapa kelemahan:

CompositionLocal membuat perilaku composable menjadi lebih sulit untuk dipertimbangkan. Saat membuat dependensi implisit, pemanggil dari composable yang menggunakannya harus memastikan bahwa nilai untuk setiap CompositionLocal terpenuhi.

Selain itu, mungkin tidak ada sumber kebenaran yang jelas untuk dependensi ini karena dependensi ini dapat mengubah bagian Komposisi mana pun. Dengan demikian, men-debug aplikasi saat terjadi masalah dapat menjadi lebih sulit karena Anda perlu menavigasi ke atas Komposisi untuk melihat tempat nilai current diberikan. Alat seperti Temukan penggunaan di IDE atau Layout inspector compose memberikan informasi yang cukup untuk menanggulangi masalah ini.

Menentukan apakah akan menggunakan CompositionLocal atau tidak

Ada kondisi tertentu yang dapat membuat CompositionLocal menjadi solusi yang baik untuk kasus penggunaan Anda:

CompositionLocal harus memiliki nilai default yang baik. Jika tidak ada nilai default, Anda harus menjamin bahwa developer sangat sulit untuk terlibat dalam sebuah situasi jika nilai untuk CompositionLocal tidak diberikan. Tidak memberikan nilai default dapat menyebabkan masalah dan kesulitan saat membuat pengujian atau melihat pratinjau composable yang menggunakan CompositionLocal akan selalu mengharuskannya diberikan secara eksplisit.

Hindari CompositionLocal untuk konsep yang tidak dianggap sebagai cakupan pohon atau cakupan sub-hierarki. CompositionLocal dapat diterima jika berpotensi untuk digunakan oleh turunan mana pun, bukan oleh beberapa turunan.

Jika kasus penggunaan Anda tidak memenuhi persyaratan ini, lihat bagian Alternatif yang perlu dipertimbangkan sebelum membuat CompositionLocal.

Contoh praktik yang buruk adalah membuat CompositionLocal yang menyimpan ViewModel layar tertentu sehingga semua composable di layar tersebut bisa mendapatkan referensi ke ViewModel untuk menjalankan beberapa logika. Ini adalah praktik yang buruk karena tidak semua composable di bawah pohon UI tertentu perlu mengetahui ViewModel. Praktik yang baik adalah meneruskan informasi yang diperlukan hanya ke composable dengan mengikuti pola status mengalir ke bawah dan peristiwa mengalir ke atas. Pendekatan ini akan membuat composable Anda menjadi lebih dapat digunakan kembali dan lebih mudah diuji.

Membuat CompositionLocal

Ada dua API untuk membuat CompositionLocal:

  • compositionLocalOf: Mengubah nilai yang diberikan selama rekomposisi akan membatalkan hanya konten yang membaca nilai current-nya.

  • staticCompositionLocalOf: Tidak seperti compositionLocalOf, operasi baca staticCompositionLocalOf tidak dilacak oleh Compose. Mengubah nilai ini akan menyebabkan rekomposisi keseluruhan lambda content, tempat CompositionLocal disediakan, bukan hanya tempat nilai current dibaca di Komposisi.

Jika nilai yang diberikan ke CompositionLocal sangat tidak mungkin berubah atau tidak akan pernah berubah, gunakan staticCompositionLocalOf untuk mendapatkan manfaat performa.

Misalnya, sistem desain aplikasi mungkin memiliki opini tentang cara menaikkan composable menggunakan bayangan untuk komponen UI. Karena elevasi yang berbeda untuk aplikasi harus diterapkan di seluruh pohon UI, kita akan menggunakan CompositionLocal. Kita menggunakan compositionLocalOf API karena nilai CompositionLocal diperoleh secara bersyarat berdasarkan tema sistem:

// LocalElevations.kt file

data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)

// Define a CompositionLocal global object with a default
// This instance can be accessed by all composables in the app
val LocalElevations = compositionLocalOf { Elevations() }

Memberikan nilai ke CompositionLocal

Composable CompositionLocalProvider mengikat nilai menjadi instance CompositionLocal untuk hierarki tertentu. Untuk memberikan nilai baru ke CompositionLocal, gunakan fungsi infiks provides yang menghubungkan kunci CompositionLocal dengan value sebagai berikut:

// MyActivity.kt file

class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // Calculate elevations based on the system theme
            val elevations = if (isSystemInDarkTheme()) {
                Elevations(card = 1.dp, default = 1.dp)
            } else {
                Elevations(card = 0.dp, default = 0.dp)
            }

            // Bind elevation as the value for LocalElevations
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... Content goes here ...
                // This part of Composition will see the `elevations` instance
                // when accessing LocalElevations.current
            }
        }
    }
}

Menggunakan CompositionLocal

CompositionLocal.current menampilkan nilai yang diberikan oleh CompositionLocalProvider terdekat yang memberikan nilai ke CompositionLocal tersebut:

@Composable
fun SomeComposable() {
    // Access the globally defined LocalElevations variable to get the
    // current Elevations in this part of the Composition
    MyCard(elevation = LocalElevations.current.card) {
        // Content
    }
}

Alternatif yang perlu dipertimbangkan

Untuk beberapa kasus penggunaan, CompositionLocal mungkin merupakan solusi yang berlebihan. Jika kasus penggunaan Anda tidak memenuhi kriteria yang ditentukan di bagian Menentukan apakah akan menggunakan CompositionLocal atau tidak, solusi lain mungkin lebih cocok untuk kasus penggunaan Anda.

Meneruskan parameter eksplisit

Menerapkan pernyataan eksplisit pada dependensi composable adalah kebiasaan yang baik. Sebaiknya Anda hanya meneruskan composable yang dibutuhkan. Untuk mendorong pemisahan dan penggunaan kembali composable, setiap composable harus memiliki sesedikit mungkin informasi.

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel.data)
}

// Don't pass the whole object! Just what the descendant needs.
// Also, don't  pass the ViewModel as an implicit dependency using
// a CompositionLocal.
@Composable
fun MyDescendant(myViewModel: MyViewModel) { /* ... */ }

// Pass only what the descendant needs
@Composable
fun MyDescendant(data: DataToDisplay) {
    // Display data
}

Inversi kontrol

Cara lain untuk menghindari penerusan dependensi yang tidak diperlukan ke composable adalah melalui inversi kontrol. Induk akan menggunakan dependensi untuk menjalankan beberapa logika, bukan turunan.

Lihat contoh berikut saat turunan harus memicu permintaan untuk memuat beberapa data:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

Bergantung pada kasusnya, MyDescendant mungkin memiliki banyak tanggung jawab. Selain itu, meneruskan MyViewModel sebagai dependensi akan membuat MyDescendant tidak terlalu dapat digunakan kembali karena keduanya kini telah digabungkan. Pertimbangkan alternatif yang tidak meneruskan dependensi ke dalam turunan dan menggunakan inversi prinsip kontrol yang mengakibatkan ancestor bertanggung jawab untuk menjalankan logika:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

Pendekatan ini dapat lebih cocok untuk beberapa kasus penggunaan karena memisahkan turunan dari ancestor sebelumnya. Composable ancestor cenderung menjadi lebih kompleks karena memiliki composable yang lebih rendah dan fleksibel.

Demikian pula, lambda konten @Composable dapat digunakan dengan cara yang sama untuk mendapatkan manfaat yang sama:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}