Skip to content

Latest commit

 

History

122 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Composed — Compose without Overdose.

Kotlin Version Compose Multiplatform API 21+ Maven Central API Reference WebAssembly Playground Build License

Android iOS JVM and Desktop WebAssembly


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.

✨ API highlights

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.

📦 Modules

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.

🚀 Installation

Inline

dependencies {
    implementation("io.github.w2sv:composed-animation:<version>")
    implementation("io.github.w2sv:composed-core:<version>")
    implementation("io.github.w2sv:composed-material3:<version>")
}

Version Catalog (libs.versions.toml)

[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)
}

🖥️ Playground

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:wasmJsBrowserDevelopmentRun

Without a desktop --args option, the playground opens a sample picker. To see the available sample IDs and detailed usage instructions, run

./gradlew :playground:usage

📄 License

Designed and developed by w2sv (Janek Zangenberg).

Licensed under the Apache License 2.0.

Releases

Packages

Used by

Contributors

Languages