Compose Multiplatform Modifiers
Overview
Modifiers in Compose Multiplatform are objects that modify the behavior or appearance of composables. They are applied in a chain and can affect layout, drawing, interaction, and more.
Basic Modifier Usage
Simple Modifiers
@Composable
fun BasicModifiers() {
Text(
text = "Hello World",
modifier = Modifier
.padding(16.dp)
.background(Color.Blue)
.clickable { /* handle click */ }
)
}
Modifier Chain Order
@Composable
fun ModifierOrder() {
// Order matters! Apply modifiers from outside to inside
Box(
modifier = Modifier
.fillMaxSize() // 1. Size
.padding(16.dp) // 2. Padding
.background(Color.Gray) // 3. Background
.clickable { } // 4. Interaction
) {
Text("Content")
}
}
Layout Modifiers
Size Modifiers
@Composable
fun SizeModifiers() {
Column {
// Fixed size
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Red)
)
// Width and height separately
Box(
modifier = Modifier
.width(200.dp)
.height(50.dp)
.background(Color.Blue)
)
// Fill available space
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Green)
)
// Aspect ratio
Box(
modifier = Modifier
.width(100.dp)
.aspectRatio(16f / 9f)
.background(Color.Yellow)
)
}
}
Padding and Margin
@Composable
fun PaddingExamples() {
Column {
// All sides
Text(
text = "All sides",
modifier = Modifier.padding(16.dp)
)
// Specific sides
Text(
text = "Specific sides",
modifier = Modifier.padding(
start = 8.dp,
end = 16.dp,
top = 4.dp,
bottom = 12.dp
)
)
// Horizontal and vertical
Text(
text = "Horizontal and vertical",
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 8.dp
)
)
}
}
Offset and Position
@Composable
fun OffsetAndPosition() {
Box(
modifier = Modifier.fillMaxSize()
) {
// Offset from original position
Text(
text = "Offset text",
modifier = Modifier.offset(x = 50.dp, y = 50.dp)
)
// Absolute position
Text(
text = "Absolute position",
modifier = Modifier.offset(
x = with(LocalDensity.current) { 100.toDp() },
y = with(LocalDensity.current) { 100.toDp() }
)
)
}
}
Drawing Modifiers
Background and Border
@Composable
fun BackgroundAndBorder() {
Column(spacing = 16.dp) {
// Simple background
Text(
text = "Simple background",
modifier = Modifier.background(Color.LightBlue)
)
// Background with shape
Text(
text = "Rounded background",
modifier = Modifier
.background(
color = Color.LightGreen,
shape = RoundedCornerShape(8.dp)
)
.padding(8.dp)
)
// Border
Text(
text = "With border",
modifier = Modifier
.border(
width = 2.dp,
color = Color.Black,
shape = RoundedCornerShape(4.dp)
)
.padding(8.dp)
)
// Background and border
Text(
text = "Background and border",
modifier = Modifier
.background(Color.LightYellow)
.border(
width = 1.dp,
color = Color.Orange,
shape = RoundedCornerShape(8.dp)
)
.padding(8.dp)
)
}
}
Shadow and Elevation
@Composable
fun ShadowExamples() {
Column(spacing = 16.dp) {
// Simple shadow
Card(
modifier = Modifier.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(8.dp)
)
) {
Text(
text = "Shadow card",
modifier = Modifier.padding(16.dp)
)
}
// Custom shadow
Box(
modifier = Modifier
.size(100.dp)
.background(Color.White)
.shadow(
elevation = 16.dp,
shape = CircleShape,
spotColor = Color.Blue.copy(alpha = 0.3f)
)
)
}
}
Alpha and Graphics Layer
@Composable
fun AlphaAndGraphicsLayer() {
Column(spacing = 16.dp) {
// Alpha modifier
Text(
text = "Semi-transparent",
modifier = Modifier.alpha(0.5f)
)
// Graphics layer for advanced effects
Text(
text = "Rotated text",
modifier = Modifier.graphicsLayer(
rotationX = 45f,
rotationY = 45f,
scaleX = 1.2f,
scaleY = 1.2f
)
)
// Combined effects
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Red)
.graphicsLayer(
alpha = 0.7f,
rotationZ = 45f,
translationX = 20f
)
)
}
}
Interaction Modifiers
Clickable and Selectable
@Composable
fun InteractionModifiers() {
var clickCount by remember { mutableStateOf(0) }
var isSelected by remember { mutableStateOf(false) }
Column(spacing = 16.dp) {
// Basic clickable
Text(
text = "Click me! Count: $clickCount",
modifier = Modifier
.clickable { clickCount++ }
.background(Color.LightBlue)
.padding(8.dp)
)
// Clickable with indication
Text(
text = "Clickable with indication",
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = true)
) { /* handle click */ }
.background(Color.LightGreen)
.padding(8.dp)
)
// Selectable
Text(
text = "Selectable text",
modifier = Modifier
.selectable(
selected = isSelected,
onClick = { isSelected = !isSelected }
)
.background(
if (isSelected) Color.Yellow else Color.Transparent
)
.padding(8.dp)
)
}
}
Focus and Keyboard
@Composable
fun FocusModifiers() {
var isFocused by remember { mutableStateOf(false) }
Column(spacing = 16.dp) {
// Focusable
TextField(
value = "",
onValueChange = {},
modifier = Modifier
.focusable()
.onFocusChanged { isFocused = it.isFocused }
.background(
if (isFocused) Color.LightBlue else Color.Transparent
)
)
// Keyboard actions
TextField(
value = "",
onValueChange = {},
modifier = Modifier
.onKeyEvent { event ->
if (event.key == Key.Enter) {
// Handle enter key
true
} else {
false
}
}
)
}
}
Custom Modifiers
Creating Custom Modifiers
// Simple custom modifier
fun Modifier.customBackground(color: Color) = this.background(color)
// Custom modifier with parameters
fun Modifier.roundedBackground(
color: Color,
cornerRadius: Dp = 8.dp
) = this
.background(color, RoundedCornerShape(cornerRadius))
.padding(cornerRadius)
// Custom modifier with state
fun Modifier.pulseAnimation(
isAnimating: Boolean
): Modifier = composed {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = if (isAnimating) 1.1f else 1f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
this.graphicsLayer {
scaleX = scale
scaleY = scale
}
}
Advanced Custom Modifiers
// Modifier with multiple effects
fun Modifier.elevatedCard(
elevation: Dp = 4.dp,
cornerRadius: Dp = 8.dp,
backgroundColor: Color = MaterialTheme.colorScheme.surface
): Modifier = this
.background(backgroundColor, RoundedCornerShape(cornerRadius))
.shadow(elevation, RoundedCornerShape(cornerRadius))
// Conditional modifier
fun Modifier.conditional(
condition: Boolean,
modifier: Modifier
): Modifier = if (condition) this.then(modifier) else this
// Platform-specific modifier
fun Modifier.platformSpecific(
androidModifier: Modifier = Modifier,
iosModifier: Modifier = Modifier,
desktopModifier: Modifier = Modifier,
webModifier: Modifier = Modifier
): Modifier = this.then(
when (Platform.current) {
Platform.Android -> androidModifier
Platform.IOS -> iosModifier
Platform.Desktop -> desktopModifier
Platform.Web -> webModifier
}
)
Common Modifier Patterns
Responsive Modifiers
@Composable
fun ResponsiveModifiers() {
val windowSize = rememberWindowSizeClass()
Box(
modifier = Modifier
.fillMaxSize()
.padding(
when (windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> 16.dp
WindowWidthSizeClass.Medium -> 32.dp
WindowWidthSizeClass.Expanded -> 64.dp
else -> 16.dp
}
)
) {
// Content
}
}
Theme-Aware Modifiers
@Composable
fun ThemeAwareModifiers() {
val colorScheme = MaterialTheme.colorScheme
Card(
modifier = Modifier
.background(colorScheme.surface)
.border(
width = 1.dp,
color = colorScheme.outline
)
) {
Text(
text = "Theme-aware card",
color = colorScheme.onSurface,
modifier = Modifier.padding(16.dp)
)
}
}
Animation Modifiers
@Composable
fun AnimatedModifiers() {
var isExpanded by remember { mutableStateOf(false) }
val animatedSize by animateDpAsState(
targetValue = if (isExpanded) 200.dp else 100.dp
)
Box(
modifier = Modifier
.size(animatedSize)
.background(Color.Blue)
.clickable { isExpanded = !isExpanded }
) {
Text(
text = if (isExpanded) "Expanded" else "Collapsed",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
Performance Considerations
Modifier Recomposition
@Composable
fun OptimizedModifiers() {
// Good: Stable modifier
val stableModifier = remember {
Modifier
.background(Color.Blue)
.padding(16.dp)
}
Text(
text = "Optimized",
modifier = stableModifier
)
// Bad: Modifier created in composition
Text(
text = "Not optimized",
modifier = Modifier
.background(Color.Blue) // Recreated every recomposition
.padding(16.dp)
)
}
Conditional Modifiers
@Composable
fun ConditionalModifiers() {
var showBorder by remember { mutableStateOf(false) }
// Good: Use conditional modifier
Text(
text = "Conditional border",
modifier = Modifier
.background(Color.LightBlue)
.conditional(
condition = showBorder,
modifier = Modifier.border(2.dp, Color.Black)
)
)
// Alternative: Use then() directly
Text(
text = "Alternative approach",
modifier = Modifier
.background(Color.LightBlue)
.then(
if (showBorder) Modifier.border(2.dp, Color.Black)
else Modifier
)
)
Button(onClick = { showBorder = !showBorder }) {
Text("Toggle border")
}
}
Best Practices
1. Modifier Order
// Good: Logical order
Box(
modifier = Modifier
.fillMaxSize() // Size first
.padding(16.dp) // Then padding
.background(Color.Blue) // Then background
.clickable { } // Then interaction
)
// Bad: Confusing order
Box(
modifier = Modifier
.clickable { } // Interaction before size
.fillMaxSize()
.background(Color.Blue)
.padding(16.dp)
)
2. Reusable Modifiers
// Create reusable modifier combinations
val cardModifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.shadow(4.dp)
.padding(16.dp)
@Composable
fun MyCard(content: @Composable () -> Unit) {
Card(modifier = cardModifier) {
content()
}
}
3. Platform-Specific Modifiers
fun Modifier.adaptivePadding(
mobile: Dp = 16.dp,
tablet: Dp = 24.dp,
desktop: Dp = 32.dp
): Modifier = this.padding(
when (Platform.current) {
Platform.Android, Platform.IOS -> mobile
Platform.Desktop -> desktop
Platform.Web -> tablet
}
)
4. Testing Modifiers
@Test
fun testCustomModifier() {
composeTestRule.setContent {
Text(
text = "Test",
modifier = Modifier.customBackground(Color.Red)
)
}
// Verify the modifier was applied correctly
composeTestRule.onNodeWithText("Test")
.assertBackgroundColor(Color.Red)
}
This comprehensive guide covers all aspects of Compose Multiplatform Modifiers, from basic usage to advanced custom implementations and best practices for cross-platform development.