Small Compose helpers you keep pasting from project to project.
Composed is a lightweight utility library for Compose Multiplatform that fills in recurring gaps around Compose APIs. It provides focused helpers for effects, snapshot state and savers, modifier composition, layout, gesture coordination, focus handling, animations, and Material 3 behavior — without introducing a framework, design system, or collection of opinionated UI components.
Platform-specific utilities stay explicitly scoped to their respective targets, while the common API remains usable across supported Compose Multiplatform platforms.
See the API reference for the full documentation.
Web/Wasm support follows Compose Multiplatform's Beta status.
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 contains Compose Desktop and Wasm browser apps for
interactively testing visual and behavioral APIs.
Try the playground web app in your browser.
Run the desktop app with:
./gradlew :playground:run [--args=<sample-id>]Or run the Wasm app in a development server with:
./gradlew :playground:wasmJsBrowserDevelopmentRunWithout a desktop --args option, the playground opens a sample picker.
To see the available sample IDs and detailed usage instructions, run
./gradlew :playground:usageDesigned and developed by w2sv (Janek Zangenberg).
Licensed under the Apache License 2.0.
