Reusable building blocks and utilities for Compose Multiplatform.
Composed extends Compose Multiplatform with focused APIs for recurring UI problems — from small utilities for state, effects, and modifier composition to reusable animation controllers, layouts, gesture and focus coordination, and Material 3 extensions. The library aims to fill practical gaps in the Compose API without introducing a framework, design system, or opinionated component library.
See the API reference for the full documentation, or check out the interactive Playground in the browser (currently not looking great on phone screens tho).
Modifier composition
Conditionally build modifier chains without repeatedly starting from Modifier:
Modifier
.padding(16.dp)
.then {
when {
isSelected -> background(Color.Green)
isDisabled -> alpha(0.5f)
else -> this
}
}
.thenIf(isFocused) {
border(1.dp, Color.Blue)
}
.thenIfNotNull(backgroundColor) {
background(it)
}Shake animation
Apply a configurable horizontal shake animation:
val shakeController = rememberShakeController(
amplitude = 20.dp,
durationMillis = 400,
frequencyHz = 8f,
decay = 0.5f
)
Box(modifier = Modifier.shakenBy(shakeController))
scope.launch {
shakeController.shake()
}Lazy grid item entrances
Stagger lazy grid items from the edge associated with the current scroll direction:
val gridState = rememberLazyGridState()
val entranceState = rememberLazyGridItemEntranceState(gridState)
LazyVerticalGrid(
columns = GridCells.Fixed(3),
state = gridState
) {
items(
items = products,
key = { it.id }
) { product ->
ProductCard(
product = product,
modifier = Modifier.animateLazyGridItemEntrance(
itemKey = product.id,
state = entranceState,
delay = LazyGridItemEntranceDelay.diagonal(
mainAxisInterval = 200.milliseconds,
crossAxisInterval = 100.milliseconds
)
)
)
}
}The API also supports horizontal grids. You can implement your own LazyGridItemEntranceDelay strategies, configure whether an item animation should be shown on every composition or only once per key, and more.
Animated spacing rows and columns
Using AnimatedVisibility inside a stock Column(verticalArrangement = Arrangement.spacedBy(...)) leaves the
arrangement spacing outside the visibility animation. The child collapses, but the full gap remains until composition
changes, which can produce an empty gap or a visible jump. AnimatedSpacingColumn animates that spacing together with
the child's occupied height and keeps the gaps on both sides symmetric. AnimatedSpacingRow provides the equivalent
behavior for a horizontal layout.
// The 12.dp gaps do not participate in AnimatedVisibility's transition.
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
AnimatedVisibility(visible = firstVisible) { FirstFilter() }
AnimatedVisibility(visible = secondVisible) { SecondFilter() }
}Use the animated-spacing scope instead:
@OptIn(ExperimentalAnimatedSpacingApi::class)
@Composable
fun FilterList(filters: List<Filter>, selectedFilters: Set<Filter>) {
AnimatedSpacingColumn(
spacing = 12.dp,
horizontalAlignment = Alignment.Start,
animation = AnimatedSpacingColumnAnimation(animationSpec = spring())
) {
filters.forEach { filter ->
AnimatedVisibility(
visible = filter in selectedFilters,
label = "${filter.id}:visibility"
) {
FilterChip(
selected = true,
onClick = { /* ... */ },
label = { Text(filter.label) }
)
}
}
}
}Both layouts retain the respective stock scope's weight and alignment modifiers, including alignment lines and row
baselines. Animated weighted children progressively release and redistribute their allocation as they disappear.
Ordinary weighted children follow the stock Row and Column allocation behavior.
These are eager experimental layouts, not lazy containers or drop-in replacements for every Row/Column arrangement.
They support fixed spacing, and animated weight redistribution costs more than ordinary weight measurement. See the API
reference for the complete behavior and limitations, or read the concise
implementation notes for measurement, rounding, and performance details.
Snackbar launching
Show snackbars from event handlers without manually carrying around a SnackbarHostState, CoroutineScope, and, on Android, a Context:
val snackbarLauncher = rememberSnackbarLauncher(snackbarHostState)
Button(
onClick = {
snackbarLauncher.show { MySnackbarVisuals(message = getString(R.string.saved)) }
}
) {
Text("Save")
}Compared to the usual pattern:
val scope = rememberCoroutineScope()
val context = LocalContext.current
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar(
MySnackbarVisuals(
message = context.getString(R.string.saved)
)
)
}
}
) {
Text("Save")
}SnackbarLauncher keeps coroutine launching and snackbar presentation behind one non-suspending API while still exposing the current snackbar state and explicit replacement or dismissal operations.
For suspending snackbar display, you may use the SnackbarController.
Focus clearing
Coordinate focus clearing from anywhere in the composition without passing around a FocusManager:
val focusClearingController = rememberFocusClearingController()
focusClearingController.Bind()
Column(modifier = Modifier.clearFocusOnTap(focusClearingController)) {
// ...
}
Button(onClick = focusClearingController::requestClearFocus) {
Text("Clear focus")
}FocusClearingController can also automatically clear focus when the IME transitions from visible
to hidden.
| Module | Description |
|---|---|
composed-core |
General-purpose Compose utilities. Contains also Android-only utilities. |
composed-animation |
Reusable animation controllers, animated spacing layouts, and lazy-grid entrances. |
composed-material3 |
Utilities and extensions for Compose Material 3 layouts, drawers, and snackbars. |
Android permission-state utilities are available separately at AugmentedPermissions.
dependencies {
implementation("io.github.w2sv:composed-animation:<version>")
implementation("io.github.w2sv:composed-core:<version>")
implementation("io.github.w2sv:composed-material3:<version>")
}[versions]
w2sv-composed = "<version>"
[libraries]
w2sv-composed-animation = { module = "io.github.w2sv:composed-animation", version.ref = "w2sv-composed" }
w2sv-composed-core = { module = "io.github.w2sv:composed-core", version.ref = "w2sv-composed" }
w2sv-composed-material3 = { module = "io.github.w2sv:composed-material3", version.ref = "w2sv-composed" }build.gradle.kts:
dependencies {
implementation(libs.w2sv.composed.animation)
implementation(libs.w2sv.composed.core)
implementation(libs.w2sv.composed.material3)
}The playground module provides Compose Desktop and Wasm browser apps for interactively exploring and testing visual and behavioral APIs.
Try the playground web app directly in your browser.
To run the Wasm app locally:
./gradlew :playground:wasmJsBrowserDevelopmentRunRun the desktop app with:
./gradlew :playground:run [--args=<sample-id>]To see the available sample IDs and detailed usage instructions:
./gradlew :playground:usageDesigned and developed by w2sv (Janek Zangenberg).
Licensed under the Apache License 2.0.
