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 đố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 áp dụng cho LayoutNode và tồn tại trong quá trình kết hợp lại thay vì được cụ thể hoá lại thông qua quá trình kết hợp trong 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 Tìm hiểu sâu về đối tượng sửa đổi trong 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 gây ra một số điểm tắc nghẽn cơ bản về hiệu suất:
- Mức hao tổn khi quản lý trạng thái: Việc quản lý trạng thái trong phạm vi này đòi hỏi các lệnh gọi
remembervà đối tượng trạng thái ảnh chụp nhanh, làm tăng kích thước bảng vị trí bằng các nhóm thành phần không cần thiết và tăng áp lực bộ nhớ. - Truy cập vòng đời tốn kém: Việc truy cập vào vòng đời của đối tượng sửa đổi đòi hỏi phải sử dụng các hiệu ứng như
DisposableEffect, điều này nhanh chóng làm tăng khối 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. - Thiếu khả năng bỏ qua: Vì hàm lambda được truyền đến
composedtrả về mộtModifier, nên trình biên dịch Compose không thể đánh dấu hàm này là có thể bỏ qua, buộc hàm này 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à cân bằng bị hỏ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 cho mỗi lệnh gọi. Việc thiếu tính năng ghi nhớ này trực tiếp phá vỡ tính bằng nhau 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ó sự truyền thay đổi thông minh: Nếu không có tính năng theo dõi tham số composable cấp cao nhất, thì không có cách nào để phân biệt thông tin đầu vào mới với thông tin đầu vào trước đó để 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 tính năng tối ưu hoá đối tượng sửa đổi bổ sung.
Các bước di chuyển chính
Ví dụ sau đây cho thấy 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 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.
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.Node(hoặcDelegatingNode) tuỳ chỉnh: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 cần quyền truy cập vào API đầu vào 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ô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 nhà máy đối tượng sửa đổi Modifier.Node hoặc @Composable.
Truy cập vào CompositionLocal
Mẫu: Đọc một CompositionLocal, 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à nhà máy đối tượng sửa đổi @Composable để truy cập vào CompositionLocal – với nhà máy @Composable, các giá trị CompositionLocal được phân giải tại vị trí gọi của nhà máy đố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 phương thức triển khai Modifier.Node tuỳ chỉnh để đọc CompositionLocal bằng CompositionLocalConsumerModifierNode.
Để biết thêm thông tin, hãy xem phần Tạo modifier tuỳ chỉnh bằng factory modifier 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()
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ả 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 hàm composable không phải bố cục
Mẫu: Đối tượng 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 phạm vi coroutine
Mẫu: Modifier.composed được dùng để thực thi rememberCoroutineScope nhằm truy cập vào đố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 gắn liền với vòng đời của đối tượng sửa đổi (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 qua các lần kết hợp lại.
Đường dẫn di chuyển: Modifier.Node được xây dựng để giữ trạng thái theo cùng một cách. 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 hiệu ứng để thực thi các thao tác liên quan đến 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).
Lộ trình di chuyển: Modifier.Node có các phương thức gọi lại trong vòng đời rõ ràng có thể dùng để thực thi cùng một thao tác. Ví dụ: LaunchedEffect thường có thể được thay thế 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 ảnh động
Mẫu: Modifier.composed sử dụng animate*AsState.
Lộ trình di chuyển: animate*AsState có thể được chia thành một modifier tuỳ chỉnh giúp giám sát các phương thức gọi lại trong vòng đời và giữ Animatable state:
// ❌ 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 } } } }