Membuat pengubah kustom

Compose menyediakan banyak pengubah untuk perilaku umum secara langsung, tetapi Anda juga dapat membuat pengubah kustom Anda sendiri.

Pengubah memiliki beberapa bagian:

  • Factory pengubah
    • Ini adalah fungsi ekstensi pada Modifier, yang menyediakan API idiomatik untuk pengubah Anda dan memungkinkan pengubah dirangkai dengan mudah. Factory pengubah menghasilkan elemen pengubah yang digunakan oleh Compose untuk mengubah UI Anda.
  • Elemen pengubah
    • Di sinilah Anda dapat menerapkan perilaku pengubah.

Ada beberapa cara untuk menerapkan pengubah kustom, bergantung pada fungsi yang diperlukan. Sering kali, cara termudah untuk menerapkan pengubah kustom adalah dengan menerapkan factory pengubah kustom yang menggabungkan factory pengubah lain yang sudah ditentukan. Jika Anda memerlukan perilaku yang lebih kustom, terapkan elemen pengubah menggunakan API Modifier.Node, yang memiliki level lebih rendah tetapi memberikan fleksibilitas lebih besar.

Menautkan pengubah yang ada

Sering kali, Anda dapat membuat pengubah kustom hanya dengan menggunakan pengubah yang ada. Misalnya, Modifier.clip() diimplementasikan menggunakan pengubah graphicsLayer. Strategi ini menggunakan elemen pengubah yang ada, dan Anda menyediakan factory pengubah kustom Anda sendiri.

Sebelum menerapkan pengubah kustom Anda sendiri, lihat apakah Anda dapat menggunakan strategi yang sama.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

Atau, jika Anda sering mengulangi grup pengubah yang sama, Anda dapat membungkusnya ke dalam pengubah Anda sendiri:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Membuat pengubah kustom menggunakan factory pengubah composable

Anda juga dapat membuat pengubah kustom menggunakan fungsi composable untuk meneruskan nilai ke pengubah yang ada. Hal ini dikenal sebagai factory pengubah composable.

Penggunaan factory pengubah composable untuk membuat pengubah juga memungkinkan penggunaan API Compose tingkat yang lebih tinggi, seperti animate*AsState dan API animasi yang didukung status Compose lainnya. Misalnya, cuplikan berikut menunjukkan pengubah yang menganimasikan perubahan alfa saat diaktifkan/dinonaktifkan:

@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return this then Modifier.graphicsLayer { this.alpha = alpha }
}

Jika pengubah kustom Anda adalah metode praktis untuk memberikan nilai default dari CompositionLocal, cara termudah untuk menerapkan hal ini adalah dengan menggunakan factory pengubah composable:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

Pendekatan ini memiliki beberapa hal yang perlu diperhatikan yang dijelaskan di bawah.

Nilai CompositionLocal diselesaikan di situs panggilan factory pengubah

Saat membuat pengubah kustom menggunakan factory pengubah composable, lokal komposisi mengambil nilai dari hierarki komposisi tempat pengubah tersebut dibuat, bukan digunakan. Hal ini dapat menyebabkan hasil yang tidak diharapkan. Misalnya, ambil contoh pengubah lokal komposisi dari atas, yang diterapkan sedikit berbeda menggunakan fungsi composable:

@Composable
fun Modifier.myBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
    CompositionLocalProvider(LocalContentColor provides Color.Green) {
        // Background modifier created with green background
        val backgroundModifier = Modifier.myBackground()

        // LocalContentColor updated to red
        CompositionLocalProvider(LocalContentColor provides Color.Red) {

            // Box will have green background, not red as expected.
            Box(modifier = backgroundModifier)
        }
    }
}

Jika pengubah tidak berfungsi seperti yang Anda harapkan, gunakan Modifier.Node kustom sebagai gantinya, karena local komposisi akan diselesaikan dengan benar di situs penggunaan dan dapat diangkat dengan aman.

Pengubah fungsi composable tidak pernah dilewati

Pengubah factory composable tidak pernah dilewati karena fungsi composable yang memiliki nilai yang ditampilkan tidak dapat dilewati. Artinya, fungsi pengubah Anda akan dipanggil pada setiap rekomposisi, yang mungkin memakan biaya besar jika sering merekomposisi.

Pengubah fungsi composable harus dipanggil dalam fungsi composable

Seperti semua fungsi composable, pengubah factory composable harus dipanggil dari dalam komposisi. Hal ini membatasi tempat pengubah dapat diangkat, karena pengubah tidak pernah dapat diangkat keluar dari komposisi. Sebagai perbandingan, factory pengubah non-composable dapat diangkat keluar dari fungsi composable untuk memungkinkan penggunaan ulang yang lebih mudah dan meningkatkan performa:

val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations

@Composable
fun Modifier.composableModifier(): Modifier {
    val color = LocalContentColor.current.copy(alpha = 0.5f)
    return this then Modifier.background(color)
}

@Composable
fun MyComposable() {
    val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
}

Menerapkan perilaku pengubah kustom menggunakan Modifier.Node

Modifier.Node adalah API tingkat yang lebih rendah untuk membuat pengubah di Compose. API ini sama dengan yang diimplementasikan Compose untuk pengubahnya sendiri dan merupakan cara paling berperforma untuk membuat pengubah kustom.

Menerapkan pengubah kustom menggunakan Modifier.Node

Ada tiga bagian dalam menerapkan pengubah kustom menggunakan Modifier.Node:

  • Penerapan Modifier.Node yang menyimpan logika dan status pengubah Anda.
  • ModifierNodeElement yang membuat dan mengupdate instance node pengubah.
  • Factory pengubah opsional seperti yang dijelaskan di atas.

Class ModifierNodeElement bersifat stateless dan instance baru dialokasikan setiap rekomposisi, sedangkan class Modifier.Node dapat bersifat stateful dan akan bertahan di beberapa rekomposisi, dan bahkan dapat digunakan kembali.

Bagian berikut menjelaskan setiap bagian dan menunjukkan contoh pembuatan pengubah kustom untuk menggambar lingkaran.

Modifier.Node

Implementasi Modifier.Node (dalam contoh ini, CircleNode) mengimplementasikan fungsi pengubah kustom Anda.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Dalam contoh ini, lingkaran digambar dengan warna yang diteruskan ke fungsi pengubah.

Node mengimplementasikan Modifier.Node serta nol atau lebih jenis node. Ada berbagai jenis node berdasarkan fungsi yang diperlukan pengubah Anda. Contoh di atas harus dapat menggambar, sehingga mengimplementasikan DrawModifierNode, yang memungkinkannya mengganti metode draw.

Jenis yang tersedia adalah sebagai berikut:

Node

Penggunaan

Contoh Link

LayoutModifierNode

Modifier.Node yang mengubah cara pengukuran dan penataan konten gabungannya.

Contoh

DrawModifierNode

Modifier.Node yang digambar ke dalam ruang tata letak.

Contoh

CompositionLocalConsumerModifierNode

Dengan menerapkan antarmuka ini, Modifier.Node Anda dapat membaca lokal komposisi.

Contoh

SemanticsModifierNode

Modifier.Node yang menambahkan key-value pair semantik untuk digunakan dalam pengujian, aksesibilitas, dan kasus penggunaan serupa.

Contoh

PointerInputModifierNode

Modifier.Node yang menerima PointerInputChanges.

Contoh

ParentDataModifierNode

Modifier.Node yang menyediakan data ke tata letak induk.

Contoh

LayoutAwareModifierNode

Modifier.Node yang menerima callback onMeasured dan onPlaced.

Contoh

GlobalPositionAwareModifierNode

Modifier.Node yang menerima callback onGloballyPositioned dengan LayoutCoordinates akhir tata letak saat posisi global konten mungkin telah berubah.

Contoh

ObserverModifierNode

Modifier.Node yang menerapkan ObserverNode dapat memberikan implementasi onObservedReadsChanged sendiri yang akan dipanggil sebagai respons terhadap perubahan pada objek snapshot yang dibaca dalam blok observeReads.

Contoh

DelegatingNode

Modifier.Node yang dapat mendelegasikan pekerjaan ke instance Modifier.Node lainnya.

Hal ini dapat berguna untuk menyusun beberapa implementasi node menjadi satu.

Contoh

TraversableNode

Memungkinkan class Modifier.Node menelusuri pohon node ke atas/bawah untuk class dengan jenis yang sama atau untuk kunci tertentu.

Contoh

Node otomatis dibatalkan saat update dipanggil pada elemen yang sesuai. Karena contoh kita adalah DrawModifierNode, setiap kali update dipanggil pada elemen, node akan memicu penggambaran ulang dan warnanya akan diperbarui dengan benar. Anda dapat memilih untuk tidak ikut serta dalam validasi otomatis seperti yang dijelaskan di bawah.

ModifierNodeElement

ModifierNodeElement adalah class yang tidak dapat diubah yang menyimpan data untuk membuat atau memperbarui pengubah kustom Anda:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

Implementasi ModifierNodeElement harus mengganti metode berikut:

  1. create: Ini adalah fungsi yang membuat instance node pengubah Anda. Fungsi ini dipanggil untuk membuat node saat pengubah Anda diterapkan pertama kali. Biasanya, hal ini sama dengan membuat node dan mengonfigurasinya dengan parameter yang diteruskan ke factory pengubah.
  2. update: Fungsi ini dipanggil setiap kali pengubah ini diberikan di tempat yang sama dengan tempat node ini sudah ada, tetapi properti telah berubah. Hal ini ditentukan oleh metode equals class. Node pengubah yang dibuat sebelumnya dikirim sebagai parameter ke panggilan update. Pada tahap ini, Anda harus memperbarui properti node agar sesuai dengan parameter yang diperbarui. Kemampuan node untuk digunakan kembali dengan cara ini adalah kunci untuk peningkatan performa yang dibawa oleh Modifier.Node; oleh karena itu, Anda harus memperbarui node yang ada, bukan membuat node baru dalam metode update. Dalam contoh lingkaran, warna node diperbarui.

Selain itu, implementasi ModifierNodeElement juga perlu mengimplementasikan equals dan hashCode. update hanya akan dipanggil jika perbandingan yang sama dengan elemen sebelumnya menampilkan nilai salah (false).

Contoh di atas menggunakan class data untuk mencapainya. Metode ini digunakan untuk memeriksa apakah node perlu diupdate atau tidak. Jika elemen Anda memiliki properti yang tidak berkontribusi pada apakah node perlu diperbarui, atau Anda ingin menghindari class data karena alasan kompatibilitas biner, maka Anda dapat menerapkan equals dan hashCode secara manual, misalnya elemen pengubah padding.

Pabrik pengubah

Ini adalah antarmuka API publik pengubah Anda. Sebagian besar penerapan cukup membuat elemen pengubah dan menambahkannya ke rantai pengubah:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

Contoh lengkap

Ketiga bagian ini digabungkan untuk membuat pengubah kustom guna menggambar lingkaran menggunakan API Modifier.Node:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Situasi umum yang menggunakan Modifier.Node

Saat membuat pengubah kustom dengan Modifier.Node, berikut beberapa situasi umum yang mungkin Anda temui.

Nol parameter

Jika pengubah Anda tidak memiliki parameter, maka pengubah tersebut tidak perlu diperbarui dan, terlebih lagi, tidak perlu menjadi class data. Berikut contoh penerapan pengubah yang menerapkan jumlah padding tetap ke composable:

fun Modifier.fixedPadding() = this then FixedPaddingElement

data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
    override fun create() = FixedPaddingNode()
    override fun update(node: FixedPaddingNode) {}
}

class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
    private val PADDING = 16.dp

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val paddingPx = PADDING.roundToPx()
        val horizontal = paddingPx * 2
        val vertical = paddingPx * 2

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            placeable.place(paddingPx, paddingPx)
        }
    }
}

Mereferensikan lokal komposisi

Pengubah Modifier.Node tidak otomatis mengamati perubahan pada objek status Compose, seperti CompositionLocal. Keunggulan pengubah Modifier.Node dibandingkan dengan pengubah yang hanya dibuat dengan factory composable adalah bahwa pengubah tersebut dapat membaca nilai komposisi lokal dari tempat pengubah digunakan di hierarki UI, bukan tempat pengubah dialokasikan, menggunakan currentValueOf.

Namun, instance node pengubah tidak otomatis mengamati perubahan status. Untuk bereaksi secara otomatis terhadap perubahan komposisi lokal, Anda dapat membaca nilai saat ini di dalam cakupan:

Contoh ini mengamati nilai LocalContentColor untuk menggambar latar belakang berdasarkan warnanya. Karena ContentDrawScope mengamati perubahan snapshot, maka akan otomatis digambar ulang saat nilai LocalContentColor berubah:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

Untuk bereaksi terhadap perubahan status di luar cakupan dan memperbarui pengubah secara otomatis, gunakan ObserverModifierNode.

Misalnya, Modifier.scrollable menggunakan teknik ini untuk mengamati perubahan pada LocalDensity. Contoh yang disederhanakan ditampilkan di bawah:

class ScrollableNode :
    Modifier.Node(),
    ObserverModifierNode,
    CompositionLocalConsumerModifierNode {

    // Place holder fling behavior, we'll initialize it when the density is available.
    val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))

    override fun onAttach() {
        updateDefaultFlingBehavior()
        observeReads { currentValueOf(LocalDensity) } // monitor change in Density
    }

    override fun onObservedReadsChanged() {
        // if density changes, update the default fling behavior.
        updateDefaultFlingBehavior()
    }

    private fun updateDefaultFlingBehavior() {
        val density = currentValueOf(LocalDensity)
        defaultFlingBehavior.flingDecay = splineBasedDecay(density)
    }
}

Pengubah animasi

Penerapan Modifier.Node memiliki akses ke coroutineScope. Hal ini memungkinkan penggunaan Compose Animatable API. Misalnya, cuplikan ini mengubah CircleNode dari atas agar memudar masuk dan keluar berulang kali:

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private lateinit var alpha: Animatable<Float, AnimationVector1D>

    override fun ContentDrawScope.draw() {
        drawCircle(color = color, alpha = alpha.value)
        drawContent()
    }

    override fun onAttach() {
        alpha = Animatable(1f)
        coroutineScope.launch {
            alpha.animateTo(
                0f,
                infiniteRepeatable(tween(1000), RepeatMode.Reverse)
            ) {
            }
        }
    }
}

Membagikan status antar-pengubah menggunakan delegasi

Pengubah Modifier.Node dapat mendelegasikan ke node lain. Ada banyak kasus penggunaan untuk hal ini, seperti mengekstrak penerapan umum di berbagai pengubah, tetapi juga dapat digunakan untuk membagikan status umum di berbagai pengubah.

Misalnya, penerapan dasar node pengubah yang dapat diklik yang membagikan data interaksi:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

Memilih tidak ikut pembatalan otomatis node

Node Modifier.Node otomatis dibatalkan saat panggilan ModifierNodeElement yang sesuai diperbarui. Terkadang, dalam pengubah yang lebih kompleks, Anda mungkin ingin menonaktifkan perilaku ini untuk memiliki kontrol yang lebih terperinci atas kapan pengubah Anda membatalkan validasi fase.

Hal ini dapat sangat berguna jika pengubah kustom Anda mengubah tata letak dan penggambaran. Dengan memilih untuk tidak menggunakan pembatalan otomatis, Anda hanya dapat membatalkan gambar saat hanya properti terkait gambar, seperti color, yang berubah, dan tidak membatalkan tata letak. Hal ini dapat meningkatkan performa pengubah Anda.

Contoh hipotetisnya ditunjukkan di bawah dengan pengubah yang memiliki lambda color, size, dan onClick sebagai properti. Pengubah ini hanya membatalkan validasi yang diperlukan, dan melewati pembatalan validasi yang tidak diperlukan:

class SampleInvalidatingNode(
    var color: Color,
    var size: IntSize,
    var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
    override val shouldAutoInvalidate: Boolean
        get() = false

    private val clickableNode = delegate(
        ClickablePointerInputNode(onClick)
    )

    fun update(color: Color, size: IntSize, onClick: () -> Unit) {
        if (this.color != color) {
            this.color = color
            // Only invalidate draw when color changes
            invalidateDraw()
        }

        if (this.size != size) {
            this.size = size
            // Only invalidate layout when size changes
            invalidateMeasurement()
        }

        // If only onClick changes, we don't need to invalidate anything
        clickableNode.update(onClick)
    }

    override fun ContentDrawScope.draw() {
        drawRect(color)
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val size = constraints.constrain(size)
        val placeable = measurable.measure(constraints)
        return layout(size.width, size.height) {
            placeable.place(0, 0)
        }
    }
}