Skip to content

Latest commit

 

History

123 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


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).

✨ 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 provides Compose Desktop and Wasm browser apps for interactively exploring and testing visual and behavioral APIs.

Web

Try the playground web app directly in your browser.

To run the Wasm app locally:

./gradlew :playground:wasmJsBrowserDevelopmentRun

Desktop

Run the desktop app with:

./gradlew :playground:run [--args=<sample-id>]

To see the available sample IDs and detailed usage instructions:

./gradlew :playground:usage

📄 License

Designed and developed by w2sv (Janek Zangenberg).

Licensed under the Apache License 2.0.

Releases

Packages

Used by

Contributors

Languages