import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composables.uripainter.rememberUriPainter
import com.composeunstyled.CrossAxisAlignment
import com.composeunstyled.ProvideWindowWidthBreakpoints
import com.composeunstyled.Stack
import com.composeunstyled.StackOrientation
import com.composeunstyled.WidthBreakpoint
import com.composeunstyled.WindowWidthBreakpoints
import com.composeunstyled.buildModifier
import com.composeunstyled.currentWindowWidthBreakpoint
private val Compact = WidthBreakpoint("compact")
private val Medium = WidthBreakpoint("medium")
private val Expanded = WidthBreakpoint("expanded")
private val DemoWidthBreakpoints = WindowWidthBreakpoints {
Compact startsAt 0.dp
Medium startsAt 600.dp
Expanded startsAt 840.dp
}
@Composable
fun BreakpointsDemo() {
ProvideWindowWidthBreakpoints(DemoWidthBreakpoints) {
val widthBreakpoint = currentWindowWidthBreakpoint()
val cardShape = RoundedCornerShape(24.dp)
val imageShape = RoundedCornerShape(18.dp)
val imagePainter = rememberUriPainter(
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee" +
"?auto=format&fit=crop&w=1200&q=80",
)
Stack(
modifier = Modifier
.widthIn(max = if (widthBreakpoint isAtLeast Expanded) 860.dp else 360.dp)
.dropShadow(
shape = cardShape,
shadow = Shadow(
radius = 28.dp,
spread = 0.dp,
offset = DpOffset(x = 0.dp, y = 14.dp),
color = Color(0xFF18181B),
alpha = 0.16f,
),
)
.clip(cardShape)
.background(Color.White)
.padding(14.dp),
orientation = if (widthBreakpoint isAtLeast Expanded) {
StackOrientation.Horizontal
} else {
StackOrientation.Vertical
},
crossAxisAlignment = CrossAxisAlignment.Start,
spacing = 18.dp,
) {
Image(
painter = imagePainter,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(imageShape)
.background(Color(0xFFE4E4E7)) then buildModifier {
if (widthBreakpoint isAtLeast Expanded) {
add(Modifier.size(width = 320.dp, height = 280.dp))
} else {
add(Modifier.fillMaxWidth().height(280.dp))
}
},
)
Stack(
modifier = Modifier then buildModifier {
if (widthBreakpoint isAtLeast Expanded) {
add(Modifier.weight(1f))
} else {
add(Modifier.fillMaxWidth())
}
},
orientation = StackOrientation.Vertical,
spacing = 12.dp,
) {
BasicText(
text = "Adaptive layouts",
style = TextStyle(
color = Color(0xFF18181B),
fontSize = 24.sp,
lineHeight = 30.sp,
),
)
BasicText(
text = "This card switches from vertical to horizontal at ${Expanded.name}",
style = TextStyle(
color = Color(0xFF52525B),
fontSize = 15.sp,
lineHeight = 22.sp,
),
)
}
}
}
}Features
- Define custom width and/or height breakpoints.
- Build responsive layouts around named semantics instead of copy-pasting dimensions.
- Emit new state only when the resolved breakpoint changes, not on every window resize.
Installation
implementation("com.composables:composeunstyled-breakpoints")
Anatomy
val Compact = WidthBreakpoint("compact")
val Medium = WidthBreakpoint("medium")
val widthBreakpoints = WindowWidthBreakpoints {
Compact startsAt 0.dp
Medium startsAt 600.dp
}
ProvideWindowWidthBreakpoints(widthBreakpoints) {
val widthBreakpoint = currentWindowWidthBreakpoint()
}
Concepts
WidthBreakpoint/HeightBreakpointdefine a named breakpoint.WindowWidthBreakpoints/WindowHeightBreakpointsdefine the minimum window size where each breakpoint starts.- Every breakpoint scale must include one breakpoint that starts at
0.dp. ProvideWindowWidthBreakpoints/ProvideWindowHeightBreakpointsmake one breakpoint scale available to their content scope.ProvideWindowBreakpointsmakes both width and height breakpoint scales available to its content scope.currentWindowWidthBreakpoint()/currentWindowHeightBreakpoint()return a new resolved breakpoint only when the current window crosses into another breakpoint.
Code Examples
Building responsive layouts
Define the window widths your layout should respond to using WindowWidthBreakpoints.
Then use ProvideWindowWidthBreakpoints near the root of your app to make the current breakpoint available down the UI tree.
Use currentWindowWidthBreakpoint() to access the currently resolved breakpoint:
val Compact = WidthBreakpoint("compact")
val Medium = WidthBreakpoint("medium")
val Expanded = WidthBreakpoint("expanded")
val widthBreakpoints = WindowWidthBreakpoints {
Compact startsAt 0.dp
Medium startsAt 600.dp
Expanded startsAt 840.dp
}
ProvideWindowWidthBreakpoints(widthBreakpoints) {
val widthBreakpoint = currentWindowWidthBreakpoint()
val expanded = widthBreakpoint isAtLeast Expanded
if (expanded) {
BasicText("Expanded layout")
} else {
BasicText("Compact layout")
}
}
Using height breakpoints
Use ProvideWindowHeightBreakpoints with currentWindowHeightBreakpoint() when layout behavior depends on available height:
val Short = HeightBreakpoint("short")
val Tall = HeightBreakpoint("tall")
val heightBreakpoints = WindowHeightBreakpoints {
Short startsAt 0.dp
Tall startsAt 720.dp
}
ProvideWindowHeightBreakpoints(heightBreakpoints) {
val heightBreakpoint = currentWindowHeightBreakpoint()
val canShowDetails = heightBreakpoint isAtLeast Tall
}
API Reference
WidthBreakpoint
| Parameter | Type | Description |
|---|---|---|
name |
String |
HeightBreakpoint
| Parameter | Type | Description |
|---|---|---|
name |
String |
WindowWidthBreakpoints
| Parameter | Type | Description |
|---|---|---|
breakpoints |
BreakpointScale<WidthBreakpoint> |
WindowWidthBreakpoints
| Parameter | Type | Description |
|---|---|---|
content |
WindowWidthBreakpointsBuilder.() -> Unit |
WindowHeightBreakpoints
| Parameter | Type | Description |
|---|---|---|
breakpoints |
BreakpointScale<HeightBreakpoint> |
WindowHeightBreakpoints
| Parameter | Type | Description |
|---|---|---|
content |
WindowHeightBreakpointsBuilder.() -> Unit |
ResolvedWidthBreakpoint
| Parameter | Type | Description |
|---|---|---|
value |
WidthBreakpoint |
|
name |
String |
ResolvedHeightBreakpoint
| Parameter | Type | Description |
|---|---|---|
value |
HeightBreakpoint |
|
name |
String |
ProvideWindowBreakpoints
| Parameter | Type | Description |
|---|---|---|
width |
WindowWidthBreakpoints |
|
height |
WindowHeightBreakpoints |
|
content |
() -> Unit |
ProvideWindowWidthBreakpoints
| Parameter | Type | Description |
|---|---|---|
breakpoints |
WindowWidthBreakpoints |
|
content |
() -> Unit |
ProvideWindowHeightBreakpoints
| Parameter | Type | Description |
|---|---|---|
breakpoints |
WindowHeightBreakpoints |
|
content |
() -> Unit |
currentWindowWidthBreakpoint
| Parameter | Type | Description |
|---|---|---|
returns |
ResolvedWidthBreakpoint |
currentWindowHeightBreakpoint
| Parameter | Type | Description |
|---|---|---|
returns |
ResolvedHeightBreakpoint |