Modifier.composed diperkenalkan di Compose 1.0 untuk memungkinkan Anda mengakses
elemen komposisi dari pengubah. Misalnya, salah satu kasus penggunaan utama adalah membuat
pengubah stateful yang mengingat status lokal dan membagikannya dengan pengubah
lain di factory Modifier.composed:
// ❌ BAD: Using Modifier.composed is no longer recommended fun Modifier.pressScale( pressedScale: Float = 0.95f, onClick: () -> Unit ): Modifier = composed( inspectorInfo = debugInspectorInfo { name = "pressScale" properties["pressedScale"] = pressedScale } ) { val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState( targetValue = if (isPressed) pressedScale else 1f, animationSpec = spring(), label = "pressScale" ) this .graphicsLayer { scaleX = scale scaleY = scale } .clickable( interactionSource = interactionSource, indication = null, onClick = onClick ) }
Gunakan Modifier.Node, bukan Modifier.composed, karena
Modifier.Node meningkatkan cara pengelolaan status dalam pengubah. Modifier.Node
adalah objek stateful yang berumur panjang dan dibuat sekali per
Modifier.Element yang diterapkan ke LayoutNode, dan objek ini tetap ada setelah rekomposisi
daripada diwujudkan kembali melalui komposisi pada setiap penerusan. Untuk mengetahui informasi selengkapnya tentang alasan dan cara kami mendesain Modifier.Node, lihat Penjelasan mendalam tentang Pengubah Compose.
Dokumen ini menjelaskan cara bermigrasi dari Modifier.composed ke
Modifier.Node. Untuk mengetahui informasi selengkapnya tentang penggunaan umum API ini, lihat
Menerapkan perilaku pengubah kustom menggunakan Modifier.Node.
Manfaat performa Modifier.Node
Penggunaan Modifier.composed menimbulkan beberapa hambatan performa mendasar:
- Overhead pengelolaan status: Mengelola status dalam cakupan ini memerlukan panggilan
rememberdan objek status snapshot, yang memperbesar tabel slot dengan grup komposisi yang tidak perlu dan meningkatkan tekanan memori. - Akses siklus proses yang mahal: Mengakses siklus proses pengubah memerlukan
penggunaan efek seperti
DisposableEffect, yang dengan cepat meningkatkan pekerjaan yang diperlukan untuk kasus penggunaan yang lebih sederhana. - Kurangnya kemampuan untuk dilewati: Karena lambda yang diteruskan ke
composedmenampilkanModifier, compiler Compose tidak dapat menandainya sebagai dapat dilewati, sehingga harus dieksekusi ulang setiap kali tata letak direkomposisi. - Memoisasi dan kesetaraan yang rusak: Karena fungsi ekstensi luar
sendiri bukan
@Composable, compiler tidak dapat memoisasi lambda dalam, sehingga menghasilkan alokasi lambda baru pada setiap panggilan. Kurangnya memoization ini secara langsung merusak kesetaraan pengubah (equals), karenaComposedModifiermembandingkan lambda berdasarkan referensi. Akibatnya, Compose memperlakukan pengubah sebagai berubah di setiap frame meskipun parameter bersifat statis. - Tidak ada propagasi perubahan cerdas: Tanpa pelacakan parameter composable tingkat teratas, tidak ada cara untuk membedakan input baru dengan input sebelumnya untuk propagasi perubahan cerdas.
Secara keseluruhan, bentuk API Modifier.composed mendorong penulisan kode yang mahal
dan mencegah runtime Compose menerapkan pengoptimalan pengubah tambahan.
Langkah-langkah migrasi inti
Contoh berikut menunjukkan pengubah kustom umum yang diimplementasikan dengan
Modifier.composed. Untuk konteks selengkapnya, lihat
Menerapkan perilaku pengubah kustom menggunakan Modifier.Node.
fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = composed { val density = LocalDensity.current val strokePx = with(density) { thickness.toPx() } // Drives how much of the underline is drawn: 0f -> 1f val progress = remember { Animatable(0f) } LaunchedEffect(color, thickness) { progress.snapTo(0f) progress.animateTo( targetValue = 1f, animationSpec = tween(durationMillis = animationDurationMillis) ) } drawBehind { val y = size.height - strokePx / 2 drawLine( color = color, start = Offset(0f, y), end = Offset(size.width * progress.value, y), strokeWidth = strokePx ) } }
Buat
Modifier.Node(atauDelegatingNode) kustom:private class UnderlineNode( private var color: Color, private var thickness: Dp, private var animationDurationMillis: Int ) : Modifier.Node() { fun update(color: Color, thickness: Dp, durationMillis: Int) { } }
Terapkan satu atau beberapa API tambahan
Modifier.Node, bergantung pada apa yang dibutuhkan pengubah kustom Anda (misalnya,PointerInputModifierNodejika perlu akses ke API input pointer):private class UnderlineNode( private var color: Color, private var thickness: Dp, private var animationDurationMillis: Int ) : Modifier.Node(), DrawModifierNode { private val progress = Animatable(0f) private var animationJob: Job? = null override fun onAttach() { restartAnimation() } fun update(color: Color, thickness: Dp, durationMillis: Int) { val needsRestart = this.color != color || this.thickness != thickness this.color = color this.thickness = thickness this.animationDurationMillis = durationMillis if (needsRestart) restartAnimation() } private fun restartAnimation() { animationJob?.cancel() animationJob = coroutineScope.launch { progress.snapTo(0f) progress.animateTo(1f, tween(animationDurationMillis)) } } override fun ContentDrawScope.draw() { val strokePx = thickness.toPx() val y = size.height - strokePx / 2 drawLine( color = color, start = Offset(0f, y), end = Offset(size.width * progress.value, y), strokeWidth = strokePx ) drawContent() } }
Buat
ModifierNodeElementyang membuat dan memperbarui node kustom Anda:private class UnderlineElement( private val color: Color, private val thickness: Dp, private val animationDurationMillis: Int ) : ModifierNodeElement<UnderlineNode>() { override fun create() = UnderlineNode(color, thickness, animationDurationMillis) override fun update(node: UnderlineNode) { node.update(color, thickness, animationDurationMillis) } override fun InspectorInfo.inspectableProperties() { name = "underline" properties["color"] = color properties["thickness"] = thickness properties["animationDurationMillis"] = animationDurationMillis } override fun hashCode(): Int { var result = color.hashCode() result = 31 * result + thickness.hashCode() result = 31 * result + animationDurationMillis.hashCode() return result } override fun equals(other: Any?): Boolean { if (this === other) return true val otherElement = other as? UnderlineElement ?: return false return color == otherElement.color && thickness == otherElement.thickness && animationDurationMillis == otherElement.animationDurationMillis } }
Perbarui factory pengubah untuk mengarah ke
ModifierNodeElement:fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = this then UnderlineElement(color, thickness, animationDurationMillis)
Resep migrasi umum
Resep berikut menunjukkan cara memigrasikan pola umum dari Modifier.composed
ke Modifier.Node atau @Composable factory pengubah.
Mengakses CompositionLocal
Pola: Membaca satu CompositionLocal seperti LocalDensity,
Theme, atau LocalView.
Jalur migrasi: Tandai pengubah dengan @Composable. Ada perbedaan semantik
antara menggunakan pengubah composed dan factory pengubah @Composable
untuk mengakses CompositionLocal—dengan factory @Composable,
nilai CompositionLocal diselesaikan di situs panggilan factory pengubah. Jika ini bukan perilaku yang diinginkan, gunakan penerapan Modifier.Node kustom yang membaca CompositionLocal menggunakan CompositionLocalConsumerModifierNode.
Untuk mengetahui informasi selengkapnya, lihat Membuat pengubah kustom menggunakan factory pengubah composable.
// ❌ BAD: Using Modifier.composed to read a single CompositionLocal fun Modifier.themedContainerBorder(): Modifier = composed { Modifier.border( BorderStroke( width = 2.dp, color = LocalColorScheme.current.primaryColor, ) ) .clipToBounds() }
// ✅ GOOD: If the modifier is @Composable, it should be able to access the locals. @Composable fun Modifier.themedContainerBorder() = this then Modifier.border( BorderStroke( width = 2.dp, color = MyTheme.mainColor, ) ) .clipToBounds()
Pola: Membaca CompositionLocal yang mungkin diterapkan ke pengubah
berikutnya.
Jalur migrasi: Buat Modifier.Node kustom yang menerapkan
CompositionLocalConsumerModifierNode dan menggabungkan semua kemampuan pengubah.
// ❌ BAD: Using Modifier.composed to read a CompositionLocal then using it in another modifier. fun Modifier.adaptiveAccessibilityPadding(basePadding: Dp): Modifier = composed { // Reading LocalThemePadding.current.small (CompositionLocal) val extraPadding = LocalThemePadding.current.small Modifier.padding(basePadding + extraPadding) }
// ✅ GOOD: A custom Modifier that combines the capabilities of both (layout and composition local reader) modifiers. fun Modifier.adaptiveAccessibilityPadding(basePadding: Dp): Modifier = this.then(AdaptivePaddingElement(basePadding)) private data class AdaptivePaddingElement( val basePadding: Dp, ) : ModifierNodeElement<AdaptivePaddingNode>() { override fun create() = AdaptivePaddingNode(basePadding) override fun update(node: AdaptivePaddingNode) { node.basePadding = basePadding } override fun InspectorInfo.inspectableProperties() { name = "adaptiveAccessibilityPadding" properties["basePadding"] = basePadding } } private class AdaptivePaddingNode( var basePadding: Dp, ) : Modifier.Node(), LayoutModifierNode, CompositionLocalConsumerModifierNode { override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { val extraPadding = currentValueOf(LocalThemePadding).small val total = (basePadding + extraPadding).roundToPx() val horizontal = total * 2 val vertical = total * 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(total, total) } } }
Mengakses fungsi composable non-layout
Pola: Pengubah perlu mengakses fungsi yang diberi anotasi dengan
@Composable dan menampilkan objek (misalnya, colorResource atau
ScrollableDefaults.flingBehavior).
Jalur migrasi: Anotasi pengubah dengan @Composable.
// ❌ BAD: Using Modifier.composed to access a composable function such as colorResource fun Modifier.niceBackground() = composed { // Reading composable function colorResource val gradientColor1 = colorResource(R.color.my_special_color) background(color = gradientColor1, shape = CircleShape) }
// ✅ GOOD: A modifier can be annotation with @Composable to reference composable functions. @Composable // Modifier can be Composable itself. private fun Modifier.niceBackground(): Modifier { val gradientColor1 = colorResource(R.color.my_special_color) return this.background(color = gradientColor1, shape = CircleShape) }
Mengakses cakupan coroutine
Pola: Modifier.composed digunakan untuk mengeksekusi rememberCoroutineScope guna
mengakses objek coroutineScope untuk meluncurkan coroutine.
Jalur migrasi: Gunakan Modifier.Node kustom, yang memiliki properti coroutineScope
yang terikat dengan siklus proses pengubah (seperti rememberCoroutineScope
di dalam Modifier.composed):
// ❌ BAD: Using Modifier.composed to get access to a coroutine scope. fun Modifier.onClickAsyncComposed(onClick: suspend () -> Unit): Modifier = composed { val scope = rememberCoroutineScope() Modifier.pointerInput(onClick) { detectTapGestures { // Needs a coroutine scope to launch suspend lambda. scope.launch { onClick() } } } }
// ✅ GOOD: A custom Modifier.Node has a scoped (modifier lifecycle) coroutineScope that can be used to launch async work. fun Modifier.onClickAsync(onClick: suspend () -> Unit): Modifier = this.then(OnClickAsyncElement(onClick)) private data class OnClickAsyncElement(val onClick: suspend () -> Unit) : ModifierNodeElement<OnClickAsyncNode>() { override fun create(): OnClickAsyncNode = OnClickAsyncNode(onClick) override fun update(node: OnClickAsyncNode) { node.update(onClick) } override fun InspectorInfo.inspectableProperties() { name = "onClickAsync" properties["onClick"] = onClick } } private class OnClickAsyncNode(private var onClick: suspend () -> Unit) : DelegatingNode(), PointerInputModifierNode { private val pointerInputNode = delegate( SuspendingPointerInputModifierNode { detectTapGestures { // Modifier.Node provides `coroutineScope` directly. coroutineScope.launch { onClick() } } } ) fun update(onClick: suspend () -> Unit) { if (this.onClick != onClick) { this.onClick = onClick pointerInputNode.resetPointerInputHandler() } } override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, ) { pointerInputNode.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { pointerInputNode.onCancelPointerInput() } }
Mengingat status
Pola: Menggunakan remember di Modifier.composed untuk menyimpan status di seluruh
rekomposisi.
Jalur migrasi: Modifier.Node dibuat untuk menyimpan status dengan cara yang sama. Status dapat disimpan dalam instance, seperti properti class lainnya
dengan siklus proses yang lebih jelas:
// ❌ BAD: Using Modifier.composed to make the modifier stateful. fun Modifier.tapCountHighlightComposed(colors: List<Color>): Modifier = composed { // 1. Must use `remember` so `tapCount` isn't reset to 0 on every recomposition var tapCount by remember { mutableIntStateOf(0) } Modifier .pointerInput(colors) { detectTapGestures { tapCount++ } } .drawBehind { drawRect(colors[tapCount % colors.size]) } }
// ✅ GOOD: Modifier.Node is the recommended way of creating stateful modifiers. fun Modifier.tapCountHighlight(colors: List<Color>): Modifier = this then TapCountHighlightElement(colors) private data class TapCountHighlightElement( val colors: List<Color>, ) : ModifierNodeElement<TapCountHighlightNode>() { override fun create() = TapCountHighlightNode(colors) override fun update(node: TapCountHighlightNode) { node.updateColors(colors) } override fun InspectorInfo.inspectableProperties() { name = "tapCountHighlight" properties["colors"] = colors } } private class TapCountHighlightNode( private var colors: List<Color>, ) : DelegatingNode(), DrawModifierNode { private var tapCount = 0 // Stateful modifier, this property will survive recompositions since Modifier.Nodes are held in the modifier tree. private val pointerInputNode = delegate( SuspendingPointerInputModifierNode { detectTapGestures { tapCount++ invalidateDraw() } } ) override fun ContentDrawScope.draw() { drawRect(colors[tapCount % colors.size]) drawContent() } fun updateColors(colors: List<Color>) { this.colors = colors invalidateDraw() } }
Menggunakan efek
Pola: Menggunakan efek untuk menjalankan operasi yang terkait dengan siklus proses komposisi (misalnya, saat Modifier.composed memasuki atau keluar dari komposisi).
Jalur migrasi: Modifier.Node memiliki callback siklus proses yang jelas yang dapat
digunakan untuk menjalankan operasi yang sama. Misalnya, LaunchedEffect umumnya dapat diganti dengan menggunakan coroutineScope di dalam metode Modifier.Node onAttach:
// ❌ BAD: Using Modifier.composed to launch/run an effect. fun Modifier.logImpressionComposed( targetId: String, onLog: suspend (targetId: String) -> Unit, ): Modifier = composed { // LaunchedEffect is tied to Composition lifecycle LaunchedEffect(targetId) { onLog(targetId) } this }
// ✅ GOOD: Modifier.Node has lifecycle callbacks (e.g onAttach, onDetach) that can be used to emulate effects behaviors. fun Modifier.logImpression(targetId: String, onLog: suspend (targetId: String) -> Unit): Modifier = this.then(LogImpressionElement(targetId, onLog)) private data class LogImpressionElement( val targetId: String, val onLog: suspend (targetId: String) -> Unit, ) : ModifierNodeElement<LogImpressionNode>() { override fun create(): LogImpressionNode = LogImpressionNode(targetId, onLog) override fun update(node: LogImpressionNode) { node.update(targetId, onLog) } override fun InspectorInfo.inspectableProperties() { name = "logImpression" properties["targetId"] = targetId } } private class LogImpressionNode( var targetId: String, var onLog: suspend (targetId: String) -> Unit, ) : Modifier.Node() { private var job: Job? = null override fun onAttach() { super.onAttach() runEffect() // Uses onAttach to track modifier lifecycle. } fun update(targetId: String, onLog: suspend (targetId: String) -> Unit) { // Re-run the effect if the key (`targetId`) changed if (this.targetId != targetId) { runEffect() } this.targetId = targetId this.onLog = onLog } private fun runEffect() { job?.cancel() job = coroutineScope.launch { onLog(targetId) } } }
Menahan status animasi
Pola: Modifier.composed menggunakan animate*AsState.
Jalur migrasi: animate*AsState dapat dipecah menjadi pengubah kustom yang memantau callback siklus proses dan menyimpan status Animatable:
// ❌ BAD: Using Modifier.composed to save an animation state. fun Modifier.fadeInOnHoverComposed(isHovered: Boolean): Modifier = composed { val alpha by animateFloatAsState( targetValue = if (isHovered) 1f else 0.4f, animationSpec = tween(durationMillis = 300), label = "alphaAnimation", ) Modifier.graphicsLayer { this.alpha = alpha } }
// ✅ GOOD: Animation state can be saved in Modifier.Node like other types of stateful implementations. fun Modifier.fadeInOnHover(isHovered: Boolean): Modifier = this.then(FadeInOnHoverElement(isHovered)) private data class FadeInOnHoverElement(val isHovered: Boolean) : ModifierNodeElement<FadeInOnHoverNode>() { override fun create(): FadeInOnHoverNode = FadeInOnHoverNode(isHovered) override fun update(node: FadeInOnHoverNode) { node.update(isHovered) } override fun InspectorInfo.inspectableProperties() { name = "fadeInOnHover" properties["isHovered"] = isHovered } } private class FadeInOnHoverNode(var isHovered: Boolean) : Modifier.Node(), LayoutModifierNode { // 1. Persistent Animatable field on the Node instance private val alphaAnimatable = Animatable(if (isHovered) 1f else 0.4f) override fun onAttach() { super.onAttach() startAnimation(isHovered) } // 2. Trigger animation imperatively when `isHovered` argument changes fun update(isHovered: Boolean) { if (this.isHovered != isHovered) { this.isHovered = isHovered if (isAttached) { startAnimation(isHovered) } } } private fun startAnimation(hovered: Boolean) { val targetAlpha = if (hovered) 1f else 0.4f // Use Node's built-in coroutineScope coroutineScope.launch { alphaAnimatable.animateTo( targetValue = targetAlpha, animationSpec = tween(durationMillis = 300), ) } } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { val placeable = measurable.measure(constraints) return layout(placeable.width, placeable.height) { // Read current animation value during layout placement layer placeable.placeWithLayer(0, 0) { alpha = alphaAnimatable.value } } } }