Responsive UI

A small, focused Compose Multiplatform library for responsive layouts —
pick the right composable per screen size and let your UI swap shape from
phone to tablet to desktop without hand-rolling the plumbing.
Targets: Android · Desktop (JVM) · iOS (iosArm64, iosSimulatorArm64) · Wasm/JS
📚 API docs · auto-published from master

The demo shows the :example app — resize the window and watch AdaptiveNavigation swap between bottom-bar / rail / drawer while the slots flip through breakpoints. Run it yourself.
What you get
A companion responsive-ui-testing artifact ships setContentWithScreenWidth { }
so breakpoint-sensitive UI tests are deterministic across every platform.
Install
Kotlin Multiplatform / Compose Multiplatform
dependencyResolutionManagement {
repositories { mavenCentral() }
}
kotlin {
sourceSets.commonMain.dependencies {
implementation("io.github.nadeemiqbal:responsive-ui:1.0.0")
}
}
Android-only
dependencies {
implementation("io.github.nadeemiqbal:responsive-ui:1.0.0")
}
Gradle's variant resolution picks the right per-target artifact via the
published module metadata — no separate Android coordinate.
Swift / SwiftUI (SwiftPM)
dependencies: [
.package(url: "https://github.com/NadeemIqbal/cmp-ui-libs-responsive", exact: "1.0.0")
]
import ResponsiveUI
Quick start
import com.nadeem.responsiveui.ResponsiveView
@Composable
fun MyScreen() {
ResponsiveView(
mobile = { MobileLayout() },
tablet = { TabletLayout() },
desktop = { DesktopLayout() },
)
}
Slots are nullable — pass only what you need. Null slots render a default
placeholder; replace it app-wide via LocalResponsiveFallback.
Pick a value by screen type
val padding = responsiveValue(mobile = 8.dp, tablet = 16.dp, desktop = 24.dp)
val columns = responsiveValue(mobile = 1, tablet = 2, desktop = 4)
Generic over T — works for Dp, Int, String, lambdas, anything.
Install breakpoints once at the app root
CompositionLocalProvider(
LocalScreenBreakpoints provides ScreenBreakpoints(
watch = 280, mobile = 600, tablet = 900,
)
) {
AppContent()
}
Defaults align with Material 3's WindowSizeClass boundaries
(600 dp = Compact↔Medium, 900 dp ≈ Medium↔Expanded).
Adaptive navigation chrome
val items = listOf(
AdaptiveNavigationItem("Home", icon = { Icon(Icons.Filled.Home, null) }),
AdaptiveNavigationItem("Search", icon = { Icon(Icons.Filled.Search, null) }),
AdaptiveNavigationItem("Settings", icon = { Icon(Icons.Filled.Settings, null) }),
)
var selected by remember { mutableIntStateOf(0) }
AdaptiveNavigation(items, selectedIndex = selected, onItemSelected = { selected = it }) {
when (selected) {
-> HomeContent()
-> SearchContent()
-> SettingsContent()
}
}
| Screen | Chrome |
|---|
| Watch / Mobile | bottom NavigationBar |
| Tablet | left |
List / detail with TwoPaneLayout
var selectedId by remember { mutableStateOf<String?>(null) }
TwoPaneLayout(
showSecondary = selectedId != null,
primary = { ItemList(onSelect = { selectedId = it }) },
secondary = { ItemDetail(selectedId) },
)
- ≥ Tablet: both panes side-by-side
- < Tablet: one pane at a time —
secondary when showSecondary == true, primary otherwise
Conditional rendering
ShowOnScreenType(listOf(ScreenType.Tablet, ScreenType.Desktop)) {
SideNavigation()
}
Live screen state
val type = rememberScreenType()
val width = rememberScreenWidth()
val height = rememberScreenHeight()
Testing
Headless test compositions report containerSize.width = 0 on some
targets (notably iOS simulator), which makes breakpoint-sensitive UI
tests flaky. The companion artifact injects a deterministic width:
kotlin.sourceSets.commonTest.dependencies {
implementation("io.github.nadeemiqbal:responsive-ui-testing:1.0.0")
}
@OptIn(ExperimentalTestApi::class)
class MyResponsiveTest {
@Test
fun showsTabletPaneAt800Dp() = runComposeUiTest {
setContentWithScreenWidth(widthDp = 800) {
MyResponsiveScreen()
}
onNodeWithText("Tablet pane").assertExists()
}
}
Running the demo
The :example module is a Compose Multiplatform app that exercises every
public API in a single window — adaptive navigation, slots, value picks,
two-pane, conditional rendering, custom fallback, live metrics.
./gradlew :example:run
./gradlew :example:installDebug
./gradlew :example:wasmJsBrowserDevelopmentRun
./gradlew :example:linkDebugFrameworkIosSimulatorArm64
Resize the desktop window to watch AdaptiveNavigation swap between
bottom-bar / rail / drawer and the slots flip through breakpoints live.
Development
./gradlew :responsive-ui:compileKotlinDesktop
./gradlew :responsive-ui:compileKotlinWasmJs
./gradlew :responsive-ui:compileKotlinIosSimulatorArm64
./gradlew :responsive-ui:desktopTest
./gradlew :responsive-ui:iosSimulatorArm64Test
./gradlew :responsive-ui:test
./gradlew :responsive-ui:publishToMavenLocal
./gradlew :responsive-ui-testing:publishToMavenLocal
./gradlew :responsive-ui:assembleResponsiveUIReleaseXCFramework
Publishing
Tag a release on master (v*) and
publish.yml on macos-latest:
- Runs
publishAndReleaseToMavenCentral
- Builds the XCFramework, zips it, computes SHA-256
- Creates a GitHub Release with the zip attached
- Updates
Package.swift URL + checksum and commits it back to master
API docs are published to GitHub Pages by
docs.yml on every push to master.
License
Apache License 2.0 — see LICENSE.