Compose menyediakan banyak pengubah untuk perilaku umum secara langsung, tetapi Anda juga dapat membuat pengubah kustom sendiri.
Pengubah memiliki beberapa bagian:
- Factory pengubah
- Ini adalah fungsi ekstensi di
Modifier
, yang menyediakan API idiomatis untuk pengubah Anda dan memungkinkan pengubah digabungkan dengan mudah. Factory pengubah menghasilkan elemen pengubah yang digunakan oleh Compose untuk mengubah UI Anda.
- Ini adalah fungsi ekstensi di
- 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 lebih banyak perilaku kustom, terapkan
elemen pengubah menggunakan Modifier.Node
API, yang merupakan level lebih rendah tetapi
memberikan lebih banyak fleksibilitas.
Gabungkan pengubah yang ada
Biasanya, 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
memberikan 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 menggabungkannya 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. 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 menerapkannya 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 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 dibuat, bukan digunakan. Hal ini dapat memberikan hasil yang tidak diharapkan. Misalnya, ambil contoh pengubah lokal komposisi di 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 ini tidak berfungsi dengan baik, gunakan
Modifier.Node
kustom, karena lokal komposisi akan
di-resolve 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. Ini berarti fungsi pengubah Anda akan dipanggil pada setiap rekomposisi, yang mungkin mahal jika merekomposisi sering.
Pengubah fungsi composable harus dipanggil dalam fungsi composable
Seperti semua fungsi composable, pengubah factory composable harus dipanggil dari dalam komposisi. Ini membatasi tempat pengubah dapat diangkat, karena pengubah tidak dapat diangkat dari komposisi. Sebagai perbandingan, factory pengubah non-composable dapat diangkat dari fungsi composable untuk memungkinkan penggunaan kembali 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 level yang lebih rendah untuk membuat pengubah di Compose. Ini
adalah API yang sama dengan yang digunakan Compose untuk menerapkan pengubahnya sendiri dan merupakan
cara paling berperforma
untuk membuat pengubah kustom.
Terapkan pengubah kustom menggunakan Modifier.Node
Ada tiga bagian dalam menerapkan pengubah kustom menggunakan Modifier.Node:
- Implementasi
Modifier.Node
yang menyimpan logika dan status pengubah. ModifierNodeElement
yang membuat dan mengupdate instance node pengubah.- Factory pengubah opsional seperti dijelaskan di atas.
Class ModifierNodeElement
adalah stateless dan instance baru dialokasikan setiap
rekomposisi, sedangkan class Modifier.Node
dapat 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
) menerapkan
fungsi pengubah kustom Anda.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Dalam contoh ini, fungsi ini menggambar lingkaran dengan warna yang diteruskan ke fungsi pengubah.
Node mengimplementasikan Modifier.Node
serta nol atau beberapa jenis node. Ada
jenis node yang berbeda berdasarkan fungsi yang diperlukan pengubah Anda. Contoh
di atas harus dapat menggambar, sehingga mengimplementasikan DrawModifierNode
, yang
memungkinkannya mengganti metode gambar.
Jenis yang tersedia adalah sebagai berikut:
Node |
Penggunaan |
Contoh Link |
|
||
|
||
Menerapkan antarmuka ini memungkinkan |
||
|
||
|
||
|
||
|
||
|
||
|
||
Hal ini berguna untuk menggabungkan beberapa implementasi node menjadi satu. |
||
Mengizinkan class |
Node akan otomatis menjadi tidak valid saat update dipanggil pada elemennya 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 mengaktifkan pembatalan otomatis seperti yang dijelaskan di bawah.
ModifierNodeElement
ModifierNodeElement
adalah class tidak dapat diubah yang menyimpan data untuk membuat atau
memperbarui pengubah kustom:
// 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. Metode ini akan dipanggil untuk membuat node saat pengubah Anda pertama kali diterapkan. Biasanya, jumlah ini setara dengan membentuk node dan mengonfigurasinya dengan parameter yang diteruskan ke factory pengubah.update
: Fungsi ini dipanggil setiap kali pengubah ini diberikan di tempat yang sama dengan node ini, tetapi properti telah berubah. Hal ini ditentukan oleh metode classequals
. Node pengubah yang sebelumnya dibuat 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 merupakan kunci peningkatan performa yang dihasilkanModifier.Node
. Oleh karena itu, Anda harus mengupdate node yang ada, bukan membuat yang baru dalam metodeupdate
. Dalam contoh lingkaran kita, warna {i>node<i} diperbarui.
Selain itu, implementasi ModifierNodeElement
juga harus menerapkan equals
dan hashCode
. update
hanya akan dipanggil jika perbandingan yang sama dengan
elemen sebelumnya menampilkan nilai salah.
Contoh di atas menggunakan class data untuk mencapai ini. Metode ini digunakan untuk
memeriksa apakah node perlu diupdate atau tidak. Jika elemen memiliki properti yang tidak
berkontribusi pada apakah node perlu diupdate, atau Anda ingin menghindari class
data karena alasan kompatibilitas biner, Anda dapat menerapkan equals
dan hashCode
secara manual, misalnya elemen pengubah padding.
Factory pengubah
Ini adalah platform API publik pengubah Anda. Sebagian besar implementasi hanya membuat elemen pengubah dan menambahkannya ke rantai pengubah:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Contoh lengkap
Ketiga bagian ini bersatu untuk membuat pengubah kustom guna menggambar lingkaran
menggunakan Modifier.Node
API:
// 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 saat menggunakan Modifier.Node
Saat membuat pengubah kustom dengan Modifier.Node
, berikut adalah beberapa situasi umum yang mungkin
Anda temui.
Tidak ada parameter
Jika tidak memiliki parameter, pengubah tidak perlu diupdate dan, lebih lanjut, tidak harus berupa class data. Berikut adalah contoh implementasi pengubah yang menerapkan padding dalam jumlah 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
. Kelebihan pengubah Modifier.Node
memiliki pengubah berlebih yang baru saja dibuat dengan factory composable adalah bahwa pengubah dapat membaca
nilai lokal komposisi dari tempat pengubah digunakan dalam hierarki
UI, bukan tempat pengubah dialokasikan, menggunakan currentValueOf
.
Namun, instance node pengubah tidak secara otomatis mengamati perubahan status. Untuk secara otomatis bereaksi terhadap perubahan lokal komposisi, 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, fungsi ini 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 di LocalDensity
. Contoh yang disederhanakan ditampilkan di bawah ini:
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
Implementasi Modifier.Node
memiliki akses ke coroutineScope
. Hal ini memungkinkan
penggunaan API Compose Animatable. Misalnya, cuplikan ini memodifikasi
CircleNode
dari atas untuk memperjelas dan memudar berulang kali:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private val alpha = Animatable(1f) override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
Berbagi status antar-pengubah menggunakan delegasi
Pengubah Modifier.Node
dapat didelegasikan ke node lain. Ada banyak kasus penggunaan
untuk hal ini, seperti mengekstrak implementasi umum di berbagai pengubah,
tetapi juga dapat digunakan untuk berbagi status umum dengan seluruh pengubah.
Misalnya, implementasi dasar dari 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 validasi otomatis node
Modifier.Node
node otomatis dibatalkan saat
ModifierNodeElement
node terkait memanggil update. Terkadang, dalam pengubah yang lebih kompleks, Anda sebaiknya
tidak menggunakan perilaku ini untuk mendapatkan kontrol yang lebih mendetail terkait kapan
pengubah membatalkan fase validasi.
Hal ini dapat sangat berguna jika pengubah kustom Anda mengubah tata letak dan
gambar. Memilih tidak ikut pembatalan otomatis memungkinkan Anda membatalkan menggambar hanya jika
properti terkait gambar, seperti color
, mengubah, dan tidak membatalkan tata letak.
Tindakan ini dapat meningkatkan performa pengubah.
Contoh hipotesis hal ini ditunjukkan di bawah dengan pengubah yang memiliki lambda color
,
size
, dan onClick
sebagai properti. Pengubah ini hanya membatalkan hal yang
diperlukan, dan melewati pembatalan validasi yang tidak:
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) } } }