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 ditautkan bersama. Factory pengubah menghasilkan elemen pengubah yang digunakan oleh Compose untuk mengubah UI Anda.
- Ini adalah fungsi ekstensi pada
- Elemen pengubah
- Di sinilah Anda dapat menerapkan perilaku pengubah.
Ada beberapa cara untuk menerapkan pengubah kustom, bergantung pada
fungsi yang diperlukan. Sering kali, cara paling sederhana 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
Anda sering kali dapat membuat pengubah kustom dengan menggunakan pengubah yang ada. Misalnya, Modifier.clip()
diterapkan menggunakan pengubah graphicsLayer
. Strategi ini menggunakan elemen pengubah yang ada, dan Anda menyediakan
pabrik 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 Anda menggunakan
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 peringatan, yang dijelaskan secara mendetail di bagian berikut.
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, pertimbangkan contoh pengubah lokal komposisi yang disebutkan sebelumnya, yang diimplementasikan 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 lokal 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 akan dipanggil pada setiap rekomposisi, yang mungkin memakan biaya besar jika sering direkomposisi.
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 sebelumnya.
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
sebelumnya harus dapat menggambar, sehingga menerapkan DrawModifierNode
,
yang memungkinkannya mengganti metode draw.
Jenis yang tersedia adalah sebagai berikut:
Node |
Penggunaan |
Contoh Link |
|
||
|
||
Dengan menerapkan antarmuka ini, |
||
|
||
|
||
|
||
|
||
|
||
|
||
Hal ini dapat berguna untuk menyusun beberapa implementasi node menjadi satu. |
||
Memungkinkan class |
Node otomatis dibatalkan saat update dipanggil pada elemen
yang sesuai. Karena contoh kita adalah DrawModifierNode
, setiap kali update dipanggil di elemen, node akan memicu penggambaran ulang dan warnanya akan diperbarui dengan benar. Anda dapat memilih untuk tidak ikut serta dalam pembatalan otomatis, seperti yang dijelaskan di bagian Memilih tidak ikut serta dalam pembatalan otomatis node.
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:
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.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 metodeequals
class. Node pengubah yang dibuat sebelumnya dikirim sebagai parameter ke panggilanupdate
. 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 olehModifier.Node
; oleh karena itu, Anda harus memperbarui node yang ada, bukan membuat node baru dalam metodeupdate
. Dalam contoh lingkaran, warna node diperbarui.
Selain itu, implementasi ModifierNodeElement
juga perlu mengimplementasikan
equals
dan hashCode
. update
hanya akan dipanggil jika perbandingan persamaan dengan elemen sebelumnya menampilkan nilai salah (false).
Contoh sebelumnya menggunakan class data untuk mencapai hal ini. 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 tidak, atau Anda ingin menghindari
class data karena alasan kompatibilitas biner, Anda dapat menerapkan
equals
dan hashCode
secara manual, misalnya, elemen pengubah padding.
Pabrik pengubah
Ini adalah antarmuka API publik pengubah Anda. Sebagian besar penerapan 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 diupdate dan, terlebih lagi, tidak perlu menjadi class data. Berikut adalah 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:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
&IntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
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 dalam
contoh berikut:
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) } }
Menganimasikan pengubah
Penerapan Modifier.Node
memiliki akses ke coroutineScope
. Hal ini memungkinkan
penggunaan Compose Animatable API. Misalnya, cuplikan ini mengubah
CircleNode
yang ditampilkan sebelumnya 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. Untuk pengubah yang kompleks, Anda mungkin ingin menonaktifkan perilaku ini untuk mendapatkan kontrol yang lebih mendetail atas waktu pengubah membatalkan validasi fase.
Hal ini sangat berguna jika pengubah kustom Anda mengubah tata letak dan
penggambaran. Dengan memilih untuk tidak menggunakan invalidasi otomatis, Anda hanya dapat membatalkan gambar saat hanya
properti terkait gambar, seperti color
, berubah. Hal ini akan menghindari pembatalan tata letak dan dapat meningkatkan performa pengubah.
Contoh hipotetisnya ditunjukkan dalam contoh berikut dengan pengubah
yang memiliki lambda color
, size
, dan onClick
sebagai properti. Pengubah ini hanya membatalkan validasi yang diperlukan, dengan 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) } } }