Modifier.composed được giới thiệu trong Compose 1.0 để cho phép bạn truy cập vào các phần tử thành phần từ đối tượng sửa đổi. Ví dụ: một trường hợp sử dụng chính là tạo một đối tượng sửa đổi có trạng thái, ghi nhớ trạng thái cục bộ và chia sẻ trạng thái đó với các đối tượng sửa đổi khác trong nhà máy 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 ) }
Sử dụng Modifier.Node thay vì Modifier.composed, vì Modifier.Node cải thiện cách quản lý trạng thái trong các đối tượng sửa đổi. Modifier.Node là một đối tượng có trạng thái, tồn tại lâu dài, được tạo một lần cho mỗi Modifier.Element được áp dụng cho một LayoutNode và tồn tại sau quá trình tái kết hợp thay vì được hiện thực hoá lại thông qua quá trình kết hợp trên mọi lượt truyền. Để biết thêm thông tin về lý do và cách chúng tôi thiết kế Modifier.Node, hãy xem bài viết Nghiên cứu chuyên sâu về đối tượng sửa đổi Compose.
Tài liệu này mô tả cách di chuyển từ Modifier.composed sang Modifier.Node. Để biết thêm thông tin về cách sử dụng chung API này, hãy xem bài viết Triển khai hành vi của đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node.
Lợi ích về hiệu suất của Modifier.Node
Việc sử dụng Modifier.composed sẽ gây ra một số điểm tắc nghẽn cơ bản về hiệu suất:
- Chi phí quản lý trạng thái: Việc quản lý trạng thái trong phạm vi này yêu cầu các lệnh gọi
remembervà các đối tượng trạng thái ảnh chụp nhanh, điều này làm tăng bảng vị trí với các nhóm thành phần không cần thiết và làm tăng áp lực bộ nhớ. - Quyền truy cập tốn kém vào vòng đời: Để truy cập vào vòng đời của đối tượng sửa đổi, bạn cần sử dụng các hiệu ứng như
DisposableEffect. Điều này nhanh chóng làm tăng lượng công việc cần thiết cho các trường hợp sử dụng đơn giản hơn. - Không thể bỏ qua: Vì lambda được truyền đến
composedtrả vềModifier, nên trình biên dịch Compose không thể đánh dấu lambda đó là có thể bỏ qua, buộc trình biên dịch phải thực thi lại bất cứ khi nào bố cục kết hợp lại. - Ghi nhớ và so sánh không thành công: Vì bản thân hàm mở rộng bên ngoài không phải là
@Composable, nên trình biên dịch không thể ghi nhớ lambda bên trong, dẫn đến việc phân bổ lambda mới trên mỗi lệnh gọi. Việc thiếu tính năng ghi nhớ này sẽ trực tiếp phá vỡ tính bình đẳng của đối tượng sửa đổi (equals), vìComposedModifierso sánh các lambda theo tham chiếu. Do đó, Compose coi đối tượng sửa đổi là đã thay đổi trên mọi khung hình ngay cả khi các tham số là tĩnh. - Không có tính năng lan truyền thay đổi thông minh: Nếu không theo dõi tham số thành phần kết hợp cấp cao nhất, bạn sẽ không thể so sánh các đầu vào mới với đầu vào trước đó để lan truyền thay đổi thông minh.
Nhìn chung, hình dạng API Modifier.composed khuyến khích việc viết mã tốn kém và ngăn thời gian chạy Compose áp dụng các chế độ tối ưu hoá bổ sung cho đối tượng sửa đổi.
Các bước di chuyển cốt lõi
Ví dụ sau đây minh hoạ một đối tượng sửa đổi tuỳ chỉnh thông thường được triển khai bằng Modifier.composed. Để biết thêm thông tin, hãy xem phần Triển khai hành vi của đối tượng sửa đổi tuỳ chỉnh bằng 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 ) } }
Tạo
Modifier.Nodetuỳ chỉnh (hoặcDelegatingNode):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) { } }
Triển khai một hoặc nhiều API phụ trợ của
Modifier.Node, tuỳ thuộc vào nhu cầu của đối tượng sửa đổi tuỳ chỉnh (ví dụ:PointerInputModifierNodenếu đối tượng này cần quyền truy cập vào API đầu vào của con trỏ):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() } }
Tạo một
ModifierNodeElementđể tạo và cập nhật nút tuỳ chỉnh: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 } }
Cập nhật nhà máy đối tượng sửa đổi để trỏ đến
ModifierNodeElement:fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = this then UnderlineElement(color, thickness, animationDurationMillis)
Các công thức di chuyển phổ biến
Các công thức sau đây cho biết cách di chuyển các mẫu phổ biến từ Modifier.composed sang Modifier.Node hoặc @Composable.
Truy cập vào CompositionLocal
Mẫu: Đọc một CompositionLocal duy nhất, chẳng hạn như LocalDensity, Theme hoặc LocalView.
Lộ trình di chuyển: Đánh dấu đối tượng sửa đổi bằng @Composable. Có sự khác biệt về ngữ nghĩa giữa việc sử dụng đối tượng sửa đổi composed và đối tượng sửa đổi @Composable để truy cập vào CompositionLocal – với đối tượng @Composable, các giá trị CompositionLocal được phân giải tại vị trí gọi của đối tượng sửa đổi. Nếu đây không phải là hành vi dự kiến, hãy sử dụng một phương thức triển khai Modifier.Node tuỳ chỉnh để đọc CompositionLocal bằng cách sử dụng CompositionLocalConsumerModifierNode.
Để biết thêm thông tin, hãy xem phần Tạo đối tượng sửa đổi tuỳ chỉnh bằng cách sử dụng một nhà máy đối tượng sửa đổi kết hợp.
// ❌ 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()
Mẫu: Đọc một CompositionLocal có thể được áp dụng cho một đối tượng sửa đổi tiếp theo.
Đường dẫn di chuyển: Tạo một Modifier.Node tuỳ chỉnh triển khai CompositionLocalConsumerModifierNode và kết hợp tất cả các chức năng của đối tượng sửa đổi.
// ❌ 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) } } }
Truy cập vào một hàm composable không phải bố cục
Mẫu: Sửa đổi cần truy cập vào một hàm được chú thích bằng @Composable và trả về một đối tượng (ví dụ: colorResource hoặc ScrollableDefaults.flingBehavior).
Đường dẫn di chuyển: Chú thích đối tượng sửa đổi bằng @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) }
Truy cập vào một phạm vi coroutine
Mẫu: Modifier.composed được dùng để thực thi rememberCoroutineScope nhằm truy cập vào một đối tượng coroutineScope để chạy coroutine.
Đường dẫn di chuyển: Sử dụng Modifier.Node tuỳ chỉnh, có thuộc tính coroutineScope được liên kết với vòng đời của đối tượng sửa đổi (chẳng hạn như rememberCoroutineScope bên trong 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() } }
Ghi nhớ trạng thái
Mẫu: Sử dụng remember trong Modifier.composed để lưu trạng thái trong các lần kết hợp lại.
Đường dẫn di chuyển: Modifier.Node được tạo để giữ trạng thái theo cách tương tự. Trạng thái có thể được giữ bên trong một thực thể, giống như bất kỳ thuộc tính lớp nào khác có vòng đời rõ ràng hơn:
// ❌ 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() } }
Sử dụng hiệu ứng
Mẫu: Sử dụng các hiệu ứng để thực thi các thao tác gắn liền với vòng đời của thành phần (ví dụ: khi Modifier.composed đã nhập hoặc thoát khỏi thành phần).
Đường dẫn di chuyển: Modifier.Node có các phương thức gọi lại rõ ràng trong vòng đời mà bạn có thể dùng để thực thi các thao tác tương tự. Ví dụ: bạn thường có thể thay thế LaunchedEffect bằng cách sử dụng coroutineScope bên trong phương thức 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) } } }
Giữ trạng thái của ảnh động
Hoa văn: Modifier.composed sử dụng animate*AsState.
Đường dẫn di chuyển: animate*AsState có thể được chia thành một đối tượng sửa đổi tuỳ chỉnh theo dõi các lệnh gọi lại trong vòng đời và giữ trạng thái 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 } } } }