Portrait-first designs should not feel narrow once the handset rotates—base orientation encodes where the PSD was authored so LOWEST / HIGHEST biases can invert across rotation.
Companion docs: PLATFORMS.md · GUIDE.md · EXAMPLES.md.
- Overview
- The Problem
- The Solution
- How It Works
- API Reference
- Examples by Strategy
- Best Practices
- Platform Notes
Base orientation declares the posture your layout was authored in. During rotation AppDimens can swap LOWEST and HIGHEST so proportions stay humane.
PORTRAIT design + LANDSCAPE current -> invert LOWEST<->HIGHEST
LANDSCAPE design + PORTRAIT current -> invert LOWEST<->HIGHEST
AUTO (library default) -> skip inversion
Works with every conceptual strategy because it only selects which axis feeds the scaler.
// Portrait card mock (360x800). On Android 3.x wire rotation via inverter docs in appdimens-dynamic.
val cardWidth = 300.wdp
// Portrait: width biased token tracks short edge
// Landscape (800x360): without inversion the card can stay visually too narrow// Prefer portraitLowest() / sdpRotate equivalents from the submodule README when you ship rotation.
val cardWidth = 300.wdpBenefits:
- Automatic adaptation driven by authored orientation metadata
- Less bespoke
rememberjuggling of width versus height swaps - Cohesive with the broader kernel catalog
if (height > width) -> CURRENT = PORTRAIT
else -> CURRENT = LANDSCAPE
| Design base | Actual orientation | Invert? |
|---|---|---|
| PORTRAIT | PORTRAIT | NO |
| PORTRAIT | LANDSCAPE | YES |
| LANDSCAPE | LANDSCAPE | NO |
| LANDSCAPE | PORTRAIT | YES |
| AUTO | Any | NO |
PORTRAIT -- authored portrait-first
LANDSCAPE -- authored landscape-first
AUTO -- default (no inversion)
Compose does not expose a .portraitLowest() chain on tokens. Instead, inversion is encoded as a token suffix on the existing axis tokens, so it composes naturally with the rest of the API. The four canonical inverters apply to every strategy package (here shown for scaled; the same *Ph/*Lw/*Lh pattern repeats on auto, percent, power, logarithmic, …):
| Token | Default axis | When orientation switches |
|---|---|---|
32.sdpPh |
smallest-width (sw) | in PORTRAIT → uses HEIGHT |
32.sdpLw |
smallest-width (sw) | in LANDSCAPE → uses WIDTH |
50.hdpLw |
HEIGHT | in LANDSCAPE → uses WIDTH |
50.wdpLh |
WIDTH | in LANDSCAPE → uses HEIGHT |
The same suffix pattern is published on text tokens (32.sspPh, 32.sspLw, 50.hspLw, 50.wspLh, …).
Full suffix catalogue (axis + a / i / ia AR/multi-window flags + inverters): COMPOSE-API-CONVENTIONS.md.
Other tracks expose the conceptual inversion via builder methods on the dimension object (consult the submodule README for the exact names):
.baseOrientation(BaseOrientation.PORTRAIT)
.type(ScreenType.LOWEST)Shorthands illustrated in Section 6: .portraitLowest(), .portraitHighest(), .landscapeLowest(), .landscapeHighest().
import com.appdimens.dynamic.compose.*
import com.appdimens.dynamic.compose.auto.asdp
@Composable
fun ResponsiveCard() {
Card(
modifier = Modifier
.width(300.wdp)
.padding(16.asdp)
) {
Text("Auto adapting card")
}
}struct ResponsiveCard: View {
var body: some View {
VStack {
Text("Auto adapting card")
}
.padding(AppDimens.shared.balanced(16).toPoints())
.frame(width: AppDimens.shared.balanced(300).portraitLowest().toPoints())
}
}Container(
width: AppDimens.fixed(300).portraitLowest().calculate(context),
padding: EdgeInsets.all(AppDimens.fixed(16).calculate(context)),
child: const Text('Auto adapting card'),
)import com.appdimens.dynamic.compose.scaled.sdp
import com.appdimens.dynamic.compose.scaled.sdpPh
import com.appdimens.dynamic.compose.scaled.sdpLw
import com.appdimens.dynamic.compose.scaled.hdpLw
import com.appdimens.dynamic.compose.scaled.wdpLh
// Standard, no inversion — sw-driven sizing
Icon(
imageVector = Icons.Default.Favorite,
modifier = Modifier.size(24.sdp)
)
// Portrait-authored padding: 32.sdpPh stays sw in landscape, switches to height in portrait
Box(modifier = Modifier.padding(32.sdpPh))
// Landscape-aware width: 32.sdpLw stays sw in portrait, switches to width in landscape
Box(modifier = Modifier.width(32.sdpLw))
// Height-anchored row that becomes width-anchored on landscape (e.g. game HUD)
Box(modifier = Modifier.height(50.hdpLw))
// Width-anchored card that becomes height-anchored on landscape
Box(modifier = Modifier.width(50.wdpLh))Surface(modifier = Modifier.width(400.wdp))Do: pick portraitLowest() when your design kit assumes portrait comps; torture-test hardware rotation before release.
Do not: mix conflicting base-orientation intents without QA; rely on AUTO if you genuinely do not care about authoring posture.
| Platform | Notes |
|---|---|
| Android | Configuration changes propagate through Compose when wired per submodule guides; inverter tokens (*Ph, *Lw, *Lh) are the canonical surface — see COMPOSE-API-CONVENTIONS.md |
| iOS | Trait collection churn triggers sizing refresh; orientation builders on the dimension chain |
| Flutter | MediaQuery rebuild semantics; builder-style .portraitLowest() / .landscapeHighest() chains |
| React Native / Web | Window resize APIs supply refreshed constraints; builder chains as on Flutter |
- Compose surface (suffixes, inverters, AR / multi-window flags):
COMPOSE-API-CONVENTIONS.md - Concept ↔ API map across stacks: PLATFORMS.md
- Why these inverters exist (axis selection theory): THEORY.md §3
- Tactical patterns: GUIDE.md
- Long-form snippets: EXAMPLES.md
Jean Bodenberg -- https://github.com/bodenberg/appdimens