Start native apps faster with the Composables CLI ->

Window Breakpoints

Window breakpoint utilities for building adaptive Compose layouts.

View on GitHub
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/HeightBreakpoint define a named breakpoint.
  • WindowWidthBreakpoints/WindowHeightBreakpoints define the minimum window size where each breakpoint starts.
  • Every breakpoint scale must include one breakpoint that starts at 0.dp.
  • ProvideWindowWidthBreakpoints/ProvideWindowHeightBreakpoints make one breakpoint scale available to their content scope.
  • ProvideWindowBreakpoints makes 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