
Generates color palettes from images, including the dominant color, and integrates with Material themes. Supports various image sources and provides extensions and composables for easy implementation.
A Kotlin Multiplatform library for generating color palettes from images, including the dominant color. You can use this library in combination with MaterialKolor to generate dynamic Material themes based on images.
Supports loading images from several sources, see Loaders.
Note: This is a port of
the androidx.palette
library.
Version 4.0 is a significant rewrite. The highlights:
androidx-palette is now a dependency-free Kotlin Multiplatform module. It no longer depends
on Compose or Skiko. Palette.Builder now takes raw pixel data (IntArray + width + height)
instead of an ImageBitmap, and downscaling is a pure-Kotlin nearest-neighbour implementation.kmpalette-loader module (published as loader) containing the ImageBitmapLoader
interface, so extensions can
depend on the loader contract without pulling in all of core.com.materialkolor.palette and the core artifact is
named core.PaletteResult: a sealed Loading/Success/Error result type replaces the previous
ad-hoc state handling.ImageBitmap.generatePalette() and ImageBitmap.rememberGeneratePalette()
for when you don't need a full state object.SwatchTarget: a Kotlin-friendly sealed interface over Target, with Palette.get(target)
operator access.ByteArrayLoader moved into core. The extensions-bytearray artifact is gone.rememberBase64PaletteState, rememberNetworkPaletteState,
rememberPlatformFilePaletteState, and so on) so you no longer wire loaders up by hand.extensions-file now uses FileKit instead of Okio.macosX64 target was removed. Kotlin/Native no longer supports Intel macOS, so all
artifacts now publish macosArm64 only.MaterialTheme.colorScheme, so defaultColor and defaultOnColor are always required and the
library makes no assumption about your design system.DEFAULT_CACHE_SIZE replaces the per-class constants.com.kmpalette.extensions.painter, alongside the resource and
drawable ones. The duplicates in com.kmpalette are gone.See the Migration Guide for upgrade instructions.
| Artifact | Android | Desktop | iOS | macOS | JS | WASM |
|---|---|---|---|---|---|---|
androidx-palette |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
kmpalette-loader |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
core |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
extensions-base64 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
extensions-network |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
extensions-file |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Apple targets are Apple Silicon only: macosArm64, iosArm64, and iosSimulatorArm64. The
deprecated macosX64 (Intel macOS) target was removed in 4.0.
The library is layered so you only take on the dependencies you actually need:
androidx-palette Pure Kotlin palette generation. No Compose. Works on raw IntArray pixels.
▲
kmpalette-loader The ImageBitmapLoader<T> interface. Depends only on Compose UI graphics.
▲
core Compose state objects, Palette/Swatch extensions, and the built-in loaders.
▲ Exposes androidx-palette and kmpalette-loader via `api`.
extensions-* Optional loaders and composables for specific image sources.
core depends on androidx-palette and kmpalette-loader with api, so adding core is enough
to get all three. The extensions-* artifacts depend on core with implementation, so you must
declare core yourself alongside any extension.
I created this library because I wanted to use the
androidx.palette library in a
Compose Multiplatform app. But that library is not multiplatform, so I decided to port it.
Want to create a dynamic Material theme based on the dominant color in an image?
Check out my other Compose Multiplatform library MaterialKolor!
You can add this library to your project using Gradle. There are several optional extension libraries, see Loaders.
In libs.versions.toml:
[versions]
kmpalette = "4.0.0"
[libraries]
kmpalette-core = { module = "com.materialkolor.palette:core", version.ref = "kmpalette" }
# Optional - standalone palette generation without Compose
kmpalette-androidx-palette = { module = "com.materialkolor.palette:androidx-palette", version.ref = "kmpalette" }
# Optional - only needed if you implement ImageBitmapLoader without depending on core
kmpalette-loader = { module = "com.materialkolor.palette:loader", version.ref = "kmpalette" }
# Optional source libraries
kmpalette-extensions-base64 = { module = "com.materialkolor.palette:extensions-base64", version.ref = "kmpalette" }
kmpalette-extensions-network = { module = "com.materialkolor.palette:extensions-network", version.ref = "kmpalette" }
kmpalette-extensions-file = { module = "com.materialkolor.palette:extensions-file", version.ref = "kmpalette" }To add to a multiplatform project, add the dependency to the common source-set:
kotlin {
sourceSets {
commonMain.dependencies {
// Core library (includes Compose utilities)
implementation(libs.kmpalette.core)
// Or use just the palette generation without Compose
// implementation(libs.kmpalette.androidx.palette)
// Optional extensions based on your image source.
// These require `core` to be declared as well.
implementation(libs.kmpalette.extensions.base64)
implementation(libs.kmpalette.extensions.network)
implementation(libs.kmpalette.extensions.file)
}
}
}extensions-file does not expose FileKit transitively, so add it yourself if you need to construct
a PlatformFile:
implementation("io.github.vinceglb:filekit-core:<version>")The androidx-palette module is a full Kotlin Multiplatform port of
the androidx.palette library.
As of 4.0 it has no dependency on Compose (its only dependency is androidx.annotation), and it
can be used in any Kotlin Multiplatform project.
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kmpalette.androidx.palette)
}
}
}Because the module is Compose-free, it works on raw pixel data rather than an ImageBitmap. You
supply an ARGB IntArray along with the source dimensions:
val palette = Palette
.from(pixels, width, height)
.maximumColorCount(16)
.generate()
val vibrant = palette.vibrantSwatch
val dominant = palette.dominantSwatchThe builder also accepts a list of pre-computed swatches:
val palette = Palette.from(swatches)Useful Palette.Builder options:
| Method | Description |
|---|---|
maximumColorCount(colors) |
Maximum number of colors in the generated palette. |
resizeBitmapArea(area) |
Target pixel area to downscale to before quantizing. Defaults to 112 * 112. |
scaling(enabled) |
Enable or disable the built-in nearest-neighbour downscaling. |
setRegion(left, top, right, bottom) |
Restrict generation to a region. Plain Ints, not Rect. |
setRegionCoordinateSpace(width, height) |
Declare the coordinate space the region is expressed in (used when you pre-scaled). |
clearRegion() |
Clear a previously set region. |
addFilter(filter) / clearFilters()
|
Add or remove Palette.Filters. |
addTarget(target) / clearTargets()
|
Add or remove Targets. |
The core module includes androidx-palette as a transitive dependency, so you don't need to add
both. Use androidx-palette on its own when you want palette generation without pulling in Compose.
To see the generated KDocs, visit the API documentation.
To try the library in your browser, visit the live demo.
To use this library, you first need an ImageBitmap, or one of the input types supported by
a loader.
If you already have an ImageBitmap, core adds a Compose-aware Palette.from overload that
handles pixel extraction and downscaling for you:
val builder = Palette.from(bitmap) // scales down to Palette.DEFAULT_RESIZE_BITMAP_AREA
val builder = Palette.from(bitmap, scale = false) // use the full-size bitmap
val palette = Palette.from(bitmap) { maximumColorCount(24) }.generate()When you don't need a reusable state object, generate a palette directly.
From a coroutine:
suspend fun load(bitmap: ImageBitmap): Palette = bitmap.generatePalette()From a composable, which returns a PaletteResult and re-runs when the bitmap
changes:
@Composable
fun SomeComposable(bitmap: ImageBitmap) {
val result = bitmap.rememberGeneratePalette()
when (result) {
is PaletteResult.Loading -> CircularProgressIndicator()
is PaletteResult.Error -> Text("Failed: ${result.cause.message}")
is PaletteResult.Success -> Text("Got ${result.palette.swatches.size} swatches")
}
}There is also an overload that takes a suspending loader lambda, so the image load and the palette generation are handled together:
@Composable
fun SomeComposable(bytes: ByteArray) {
val result = rememberGeneratePalette(loader = { ByteArrayLoader.load(bytes) })
val palette = result.paletteOrNull
}Every asynchronous operation reports through PaletteResult:
public sealed interface PaletteResult {
public data class Success(val palette: Palette) : PaletteResult
public data object Loading : PaletteResult
public data class Error(val cause: Exception) : PaletteResult
public val paletteOrNull: Palette?
}Use paletteOrNull when you only care about the happy path.
You can generate a dominant color from an ImageBitmap using the rememberDominantColorState
composable. This will also provide an onColor for you to use as a text color.
@Composable
fun SomeComposable(bitmap: ImageBitmap) {
val dominantColorState = rememberDominantColorState(
defaultColor = Color.Black,
defaultOnColor = Color.White,
)
LaunchedEffect(bitmap) {
dominantColorState.updateFrom(bitmap)
}
Box(
modifier = Modifier
.width(200.dp)
.height(100.dp)
.background(dominantColorState.color)
) {
Text("Some Text", color = dominantColorState.onColor)
}
}You can also use a Painter object with the dedicated rememberPainterDominantColorState:
import com.kmpalette.extensions.painter.rememberPainterDominantColorState
@Composable
fun SomeComposable(painter: Painter) {
val dominantColorState = rememberPainterDominantColorState(
defaultColor = Color.Black,
defaultOnColor = Color.White,
)
LaunchedEffect(painter) {
dominantColorState.updateFrom(painter)
}
// ...
}Note on default colors. Every dominant-color composable requires
defaultColoranddefaultOnColor. They are shown until generation succeeds, and whenever it fails. The library does not guess a fallback, so it makes no assumption about your design system and does not depend on Material 3. Pass whatever your theme uses:// Material 3 val state = rememberBase64DominantColorState( defaultColor = MaterialTheme.colorScheme.primary, defaultOnColor = MaterialTheme.colorScheme.onPrimary, ) // or anything else val state = rememberBase64DominantColorState( defaultColor = MyTheme.colors.surface, defaultOnColor = MyTheme.colors.onSurface, )
Since the generation of the dominant color is an asynchronous operation that can fail, you can track
the results of the operation using the DominantColorState.result property, which is a
PaletteResult.
If you want to filter the dominant color, you can pass an isSwatchValid lambda:
val dominantColorState = rememberDominantColorState(
defaultColor = Color.Black,
defaultOnColor = Color.White,
isSwatchValid = { swatch -> swatch.population > 100 },
)
LaunchedEffect(bitmap) {
dominantColorState.updateFrom(bitmap)
}Swatches are checked in descending population order, and the first valid one wins.
If you want a whole color palette instead of just a dominant color, you can use
the rememberPaletteState composable. The generated Palette is exposed through
PaletteState.palette, and the full result through PaletteState.state.
Using an ImageBitmap:
@Composable
fun SomeComposable(bitmap: ImageBitmap) {
val paletteState = rememberPaletteState()
LaunchedEffect(bitmap) {
paletteState.generate(bitmap)
}
val vibrant = paletteState.palette?.vibrantSwatch
Box(
modifier = Modifier
.width(200.dp)
.height(100.dp)
.background(vibrant?.color ?: Color.White)
) {
Text(
text = "Some Text",
color = vibrant?.onColor ?: LocalContentColor.current,
)
}
}Or using a Painter:
@Composable
fun SomeComposable(painter: Painter) {
val paletteState = rememberPainterPaletteState()
LaunchedEffect(painter) {
paletteState.generate(painter)
}
// ...
}PaletteState.state is a PaletteResult, so you can render loading and error
states directly:
when (val state = paletteState.state) {
null -> Text("Nothing generated yet")
PaletteResult.Loading -> CircularProgressIndicator()
is PaletteResult.Error -> Text("Failed: ${state.cause.message}")
is PaletteResult.Success -> PaletteDisplay(state.palette)
}Alongside the standard vibrantSwatch, mutedSwatch, dominantSwatch (and friends) properties,
core adds SwatchTarget, a sealed interface that maps to androidx-palette's Target, plus an
indexed accessor:
val vibrant = palette[SwatchTarget.Vibrant]
val darkMuted = palette[SwatchTarget.MutedDark]
// Equivalent, non-operator form
val lightVibrant = palette.getSwatch(SwatchTarget.VibrantLight)SwatchTarget |
Equivalent property |
|---|---|
Vibrant |
vibrantSwatch |
VibrantDark |
darkVibrantSwatch |
VibrantLight |
lightVibrantSwatch |
Muted |
mutedSwatch |
MutedDark |
darkMutedSwatch |
MutedLight |
lightMutedSwatch |
The raw Target type works too, if you built a custom one:
val swatch = palette[Target.VIBRANT]core provides Compose Color extensions on Palette.Swatch:
| Extension | Description |
|---|---|
swatch.color |
The swatch color. |
swatch.onColor |
Title text color, guaranteed to contrast against color. |
swatch.titleTextColor() |
Same as onColor. |
swatch.bodyTextColor() |
Body text color, guaranteed to contrast against color. |
Note that DominantColorState.onColor is derived from the swatch's body text color, whereas
Swatch.onColor is the title text color.
Both state objects keep an LRU cache of results, keyed on the input. Every state function defaults
to DEFAULT_CACHE_SIZE. Pass cacheSize = 0 to disable caching, and call reset() to clear the
cache and return to the default values.
val paletteState = rememberPaletteState(cacheSize = 0)
// later
paletteState.reset()public const val DEFAULT_CACHE_SIZE: Int = 6Note that the cache is keyed on the input, so an ImageBitmap or Painter key keeps that object
alive for as long as it stays in the cache. Pass cacheSize = 0 for those inputs if that matters
to you.
A loader implements ImageBitmapLoader<T> from the kmpalette-loader module:
public interface ImageBitmapLoader<T> {
public suspend fun load(input: T): ImageBitmap
}core ships with these loaders:
| Loader | Input | Availability | Notes |
|---|---|---|---|
ByteArrayLoader |
ByteArray |
All | Was extensions-bytearray in 3.x. |
PainterLoader |
Painter |
All | Use rememberPainterLoader(). |
ResourceLoader |
DrawableResource |
All | Use rememberResourceLoader(). |
DrawableLoader |
@DrawableRes Int |
Android only | Use rememberDrawableLoader(). |
And these are available as separate artifacts:
| Artifact | Library | Loader | Input |
|---|---|---|---|
extensions-base64 |
N/A | Base64Loader |
String |
extensions-network |
Ktor | NetworkLoader |
Url |
extensions-file |
FileKit | PlatformFileLoader |
PlatformFile |
extensions-file |
N/A | FileLoader |
java.io.File (Android only) |
Every loader has a matching pair of composables:
| Input | Dominant color | Palette |
|---|---|---|
ImageBitmap |
rememberDominantColorState() |
rememberPaletteState() |
Painter |
rememberPainterDominantColorState()¹ |
rememberPainterPaletteState()¹ |
DrawableResource |
rememberResourceDominantColorState() |
rememberResourcePaletteState() |
@DrawableRes Int |
rememberDrawableDominantColorState() |
rememberDrawablePaletteState() |
String (Base64) |
rememberBase64DominantColorState() |
rememberBase64PaletteState() |
Url |
rememberNetworkDominantColorState() |
rememberNetworkPaletteState() |
PlatformFile |
rememberPlatformFileDominantColorState() |
rememberPlatformFilePaletteState() |
java.io.File |
rememberFileDominantColorState() |
rememberFilePaletteState() |
¹ In com.kmpalette.extensions.painter, matching the resource and drawable composables. All
dominant-color functions require defaultColor and defaultOnColor.
For example, the extensions-network module:
@Composable
fun SomeComposable(url: Url) {
val dominantColorState = rememberNetworkDominantColorState(
defaultColor = MaterialTheme.colorScheme.primary,
defaultOnColor = MaterialTheme.colorScheme.onPrimary,
)
LaunchedEffect(url) {
dominantColorState.updateFrom(url)
}
Box(
modifier = Modifier
.width(200.dp)
.height(100.dp)
.background(dominantColorState.color)
) {
Text("Some Text", color = dominantColorState.onColor)
}
}You can always fall back to the generic state functions and pass a loader yourself:
val paletteState = rememberPaletteState(loader = ByteArrayLoader)
LaunchedEffect(bytes) {
paletteState.generate(bytes)
}To generate a palette from a DrawableResource use rememberResourceDominantColorState or
rememberResourcePaletteState, which read the resource bytes directly:
@Composable
fun MyComposable() {
val dominantColorState = rememberResourceDominantColorState(
defaultColor = MaterialTheme.colorScheme.primary,
defaultOnColor = MaterialTheme.colorScheme.onPrimary,
)
LaunchedEffect(Unit) {
dominantColorState.updateFrom(Res.drawable.my_image)
}
}Alternatively, you can use the @Composable imageResource() to get an ImageBitmap then pass that
to the default loader:
@Composable
fun MyComposable() {
val image = imageResource(Res.drawable.my_image)
val dominantColorState = rememberDominantColorState(
defaultColor = Color.Black,
defaultOnColor = Color.White,
)
LaunchedEffect(image) {
dominantColorState.updateFrom(image)
}
}If you are migrating from version 3.x, please see the Migration Guide for detailed instructions on updating your code.
Key changes in 4.0:
com.materialkolor.palette, and kmpalette-core is now core
androidx-palette no longer depends on Compose; Palette.Builder takes IntArray pixelskmpalette-bitmap-loader was renamed to kmpalette-loader and is published as loader
extensions-bytearray and extensions-resources have been folded into core
extensions-libres has been removed with no replacementextensions-file now uses FileKit instead of Okiocom.kmpalette.extensions.painter
defaultColor and defaultOnColor are required; nothing depends on Material 3macosX64 and iosX64 targets were removedIf you have a feature request, please open an issue. If you would like to implement a feature request, refer to the Contributing section.
Contributions are always welcome! If you'd like to contribute, open a PR or an issue.
The module androidx-palette is licensed under the Apache License, Version 2.0. See
their LICENSE and their
repository here for more
information.
IntArray pixel data insteadRect-based region selection with plain Int coordinatesFor the remaining code see LICENSE for more information.
A Kotlin Multiplatform library for generating color palettes from images, including the dominant color. You can use this library in combination with MaterialKolor to generate dynamic Material themes based on images.
Supports loading images from several sources, see Loaders.
Note: This is a port of
the androidx.palette
library.
Version 4.0 is a significant rewrite. The highlights:
androidx-palette is now a dependency-free Kotlin Multiplatform module. It no longer depends
on Compose or Skiko. Palette.Builder now takes raw pixel data (IntArray + width + height)
instead of an ImageBitmap, and downscaling is a pure-Kotlin nearest-neighbour implementation.kmpalette-loader module (published as loader) containing the ImageBitmapLoader
interface, so extensions can
depend on the loader contract without pulling in all of core.com.materialkolor.palette and the core artifact is
named core.PaletteResult: a sealed Loading/Success/Error result type replaces the previous
ad-hoc state handling.ImageBitmap.generatePalette() and ImageBitmap.rememberGeneratePalette()
for when you don't need a full state object.SwatchTarget: a Kotlin-friendly sealed interface over Target, with Palette.get(target)
operator access.ByteArrayLoader moved into core. The extensions-bytearray artifact is gone.rememberBase64PaletteState, rememberNetworkPaletteState,
rememberPlatformFilePaletteState, and so on) so you no longer wire loaders up by hand.extensions-file now uses FileKit instead of Okio.macosX64 target was removed. Kotlin/Native no longer supports Intel macOS, so all
artifacts now publish macosArm64 only.MaterialTheme.colorScheme, so defaultColor and defaultOnColor are always required and the
library makes no assumption about your design system.DEFAULT_CACHE_SIZE replaces the per-class constants.com.kmpalette.extensions.painter, alongside the resource and
drawable ones. The duplicates in com.kmpalette are gone.See the Migration Guide for upgrade instructions.
| Artifact | Android | Desktop | iOS | macOS | JS | WASM |
|---|---|---|---|---|---|---|
androidx-palette |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
kmpalette-loader |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
core |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
extensions-base64 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
extensions-network |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
extensions-file |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Apple targets are Apple Silicon only: macosArm64, iosArm64, and iosSimulatorArm64. The
deprecated macosX64 (Intel macOS) target was removed in 4.0.
The library is layered so you only take on the dependencies you actually need:
androidx-palette Pure Kotlin palette generation. No Compose. Works on raw IntArray pixels.
▲
kmpalette-loader The ImageBitmapLoader<T> interface. Depends only on Compose UI graphics.
▲
core Compose state objects, Palette/Swatch extensions, and the built-in loaders.
▲ Exposes androidx-palette and kmpalette-loader via `api`.
extensions-* Optional loaders and composables for specific image sources.
core depends on androidx-palette and kmpalette-loader with api, so adding core is enough
to get all three. The extensions-* artifacts depend on core with implementation, so you must
declare core yourself alongside any extension.
I created this library because I wanted to use the
androidx.palette library in a
Compose Multiplatform app. But that library is not multiplatform, so I decided to port it.
Want to create a dynamic Material theme based on the dominant color in an image?
Check out my other Compose Multiplatform library MaterialKolor!
You can add this library to your project using Gradle. There are several optional extension libraries, see Loaders.
In libs.versions.toml:
[versions]
kmpalette = "4.0.0"
[libraries]
kmpalette-core = { module = "com.materialkolor.palette:core", version.ref = "kmpalette" }
# Optional - standalone palette generation without Compose
kmpalette-androidx-palette = { module = "com.materialkolor.palette:androidx-palette", version.ref = "kmpalette" }
# Optional - only needed if you implement ImageBitmapLoader without depending on core
kmpalette-loader = { module = "com.materialkolor.palette:loader", version.ref = "kmpalette" }
# Optional source libraries
kmpalette-extensions-base64 = { module = "com.materialkolor.palette:extensions-base64", version.ref = "kmpalette" }
kmpalette-extensions-network = { module = "com.materialkolor.palette:extensions-network", version.ref = "kmpalette" }
kmpalette-extensions-file = { module = "com.materialkolor.palette:extensions-file", version.ref = "kmpalette" }To add to a multiplatform project, add the dependency to the common source-set:
kotlin {
sourceSets {
commonMain.dependencies {
// Core library (includes Compose utilities)
implementation(libs.kmpalette.core)
// Or use just the palette generation without Compose
// implementation(libs.kmpalette.androidx.palette)
// Optional extensions based on your image source.
// These require `core` to be declared as well.
implementation(libs.kmpalette.extensions.base64)
implementation(libs.kmpalette.extensions.network)
implementation(libs.kmpalette.extensions.file)
}
}
}extensions-file does not expose FileKit transitively, so add it yourself if you need to construct
a PlatformFile:
implementation("io.github.vinceglb:filekit-core:<version>")The androidx-palette module is a full Kotlin Multiplatform port of
the androidx.palette library.
As of 4.0 it has no dependency on Compose (its only dependency is androidx.annotation), and it
can be used in any Kotlin Multiplatform project.
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kmpalette.androidx.palette)
}
}
}Because the module is Compose-free, it works on raw pixel data rather than an ImageBitmap. You
supply an ARGB IntArray along with the source dimensions:
val palette = Palette
.from(pixels, width, height)
.maximumColorCount(16)
.generate()
val vibrant = palette.vibrantSwatch
val dominant = palette.dominantSwatchThe builder also accepts a list of pre-computed swatches:
val palette = Palette.from(swatches)Useful Palette.Builder options:
| Method | Description |
|---|---|
maximumColorCount(colors) |
Maximum number of colors in the generated palette. |
resizeBitmapArea(area) |
Target pixel area to downscale to before quantizing. Defaults to 112 * 112. |
scaling(enabled) |
Enable or disable the built-in nearest-neighbour downscaling. |
setRegion(left, top, right, bottom) |
Restrict generation to a region. Plain Ints, not Rect. |
setRegionCoordinateSpace(width, height) |
Declare the coordinate space the region is expressed in (used when you pre-scaled). |
clearRegion() |
Clear a previously set region. |
addFilter(filter) / clearFilters()
|
Add or remove Palette.Filters. |
addTarget(target) / clearTargets()
|
Add or remove Targets. |
The core module includes androidx-palette as a transitive dependency, so you don't need to add
both. Use androidx-palette on its own when you want palette generation without pulling in Compose.
To see the generated KDocs, visit the API documentation.
To try the library in your browser, visit the live demo.
To use this library, you first need an ImageBitmap, or one of the input types supported by
a loader.
If you already have an ImageBitmap, core adds a Compose-aware Palette.from overload that
handles pixel extraction and downscaling for you:
val builder = Palette.from(bitmap) // scales down to Palette.DEFAULT_RESIZE_BITMAP_AREA
val builder = Palette.from(bitmap, scale = false) // use the full-size bitmap
val palette = Palette.from(bitmap) { maximumColorCount(24) }.generate()When you don't need a reusable state object, generate a palette directly.
From a coroutine:
suspend fun load(bitmap: ImageBitmap): Palette = bitmap.generatePalette()From a composable, which returns a PaletteResult and re-runs when the bitmap
changes:
@Composable
fun SomeComposable(bitmap: ImageBitmap) {
val result = bitmap.rememberGeneratePalette()
when (result) {
is PaletteResult.Loading -> CircularProgressIndicator()
is PaletteResult.Error -> Text("Failed: ${result.cause.message}")
is PaletteResult.Success -> Text("Got ${result.palette.swatches.size} swatches")
}
}There is also an overload that takes a suspending loader lambda, so the image load and the palette generation are handled together:
@Composable
fun SomeComposable(bytes: ByteArray) {
val result = rememberGeneratePalette(loader = { ByteArrayLoader.load(bytes) })
val palette = result.paletteOrNull
}Every asynchronous operation reports through PaletteResult:
public sealed interface PaletteResult {
public data class Success(val palette: Palette) : PaletteResult
public data object Loading : PaletteResult
public data class Error(val cause: Exception) : PaletteResult
public val paletteOrNull: Palette?
}Use paletteOrNull when you only care about the happy path.
You can generate a dominant color from an ImageBitmap using the rememberDominantColorState
composable. This will also provide an onColor for you to use as a text color.
@Composable
fun SomeComposable(bitmap: ImageBitmap) {
val dominantColorState = rememberDominantColorState(
defaultColor = Color.Black,
defaultOnColor = Color.White,
)
LaunchedEffect(bitmap) {
dominantColorState.updateFrom(bitmap)
}
Box(
modifier = Modifier
.width(200.dp)
.height(100.dp)
.background(dominantColorState.color)
) {
Text("Some Text", color = dominantColorState.onColor)
}
}You can also use a Painter object with the dedicated rememberPainterDominantColorState:
import com.kmpalette.extensions.painter.rememberPainterDominantColorState
@Composable
fun SomeComposable(painter: Painter) {
val dominantColorState = rememberPainterDominantColorState(
defaultColor = Color.Black,
defaultOnColor = Color.White,
)
LaunchedEffect(painter) {
dominantColorState.updateFrom(painter)
}
// ...
}Note on default colors. Every dominant-color composable requires
defaultColoranddefaultOnColor. They are shown until generation succeeds, and whenever it fails. The library does not guess a fallback, so it makes no assumption about your design system and does not depend on Material 3. Pass whatever your theme uses:// Material 3 val state = rememberBase64DominantColorState( defaultColor = MaterialTheme.colorScheme.primary, defaultOnColor = MaterialTheme.colorScheme.onPrimary, ) // or anything else val state = rememberBase64DominantColorState( defaultColor = MyTheme.colors.surface, defaultOnColor = MyTheme.colors.onSurface, )
Since the generation of the dominant color is an asynchronous operation that can fail, you can track
the results of the operation using the DominantColorState.result property, which is a
PaletteResult.
If you want to filter the dominant color, you can pass an isSwatchValid lambda:
val dominantColorState = rememberDominantColorState(
defaultColor = Color.Black,
defaultOnColor = Color.White,
isSwatchValid = { swatch -> swatch.population > 100 },
)
LaunchedEffect(bitmap) {
dominantColorState.updateFrom(bitmap)
}Swatches are checked in descending population order, and the first valid one wins.
If you want a whole color palette instead of just a dominant color, you can use
the rememberPaletteState composable. The generated Palette is exposed through
PaletteState.palette, and the full result through PaletteState.state.
Using an ImageBitmap:
@Composable
fun SomeComposable(bitmap: ImageBitmap) {
val paletteState = rememberPaletteState()
LaunchedEffect(bitmap) {
paletteState.generate(bitmap)
}
val vibrant = paletteState.palette?.vibrantSwatch
Box(
modifier = Modifier
.width(200.dp)
.height(100.dp)
.background(vibrant?.color ?: Color.White)
) {
Text(
text = "Some Text",
color = vibrant?.onColor ?: LocalContentColor.current,
)
}
}Or using a Painter:
@Composable
fun SomeComposable(painter: Painter) {
val paletteState = rememberPainterPaletteState()
LaunchedEffect(painter) {
paletteState.generate(painter)
}
// ...
}PaletteState.state is a PaletteResult, so you can render loading and error
states directly:
when (val state = paletteState.state) {
null -> Text("Nothing generated yet")
PaletteResult.Loading -> CircularProgressIndicator()
is PaletteResult.Error -> Text("Failed: ${state.cause.message}")
is PaletteResult.Success -> PaletteDisplay(state.palette)
}Alongside the standard vibrantSwatch, mutedSwatch, dominantSwatch (and friends) properties,
core adds SwatchTarget, a sealed interface that maps to androidx-palette's Target, plus an
indexed accessor:
val vibrant = palette[SwatchTarget.Vibrant]
val darkMuted = palette[SwatchTarget.MutedDark]
// Equivalent, non-operator form
val lightVibrant = palette.getSwatch(SwatchTarget.VibrantLight)SwatchTarget |
Equivalent property |
|---|---|
Vibrant |
vibrantSwatch |
VibrantDark |
darkVibrantSwatch |
VibrantLight |
lightVibrantSwatch |
Muted |
mutedSwatch |
MutedDark |
darkMutedSwatch |
MutedLight |
lightMutedSwatch |
The raw Target type works too, if you built a custom one:
val swatch = palette[Target.VIBRANT]core provides Compose Color extensions on Palette.Swatch:
| Extension | Description |
|---|---|
swatch.color |
The swatch color. |
swatch.onColor |
Title text color, guaranteed to contrast against color. |
swatch.titleTextColor() |
Same as onColor. |
swatch.bodyTextColor() |
Body text color, guaranteed to contrast against color. |
Note that DominantColorState.onColor is derived from the swatch's body text color, whereas
Swatch.onColor is the title text color.
Both state objects keep an LRU cache of results, keyed on the input. Every state function defaults
to DEFAULT_CACHE_SIZE. Pass cacheSize = 0 to disable caching, and call reset() to clear the
cache and return to the default values.
val paletteState = rememberPaletteState(cacheSize = 0)
// later
paletteState.reset()public const val DEFAULT_CACHE_SIZE: Int = 6Note that the cache is keyed on the input, so an ImageBitmap or Painter key keeps that object
alive for as long as it stays in the cache. Pass cacheSize = 0 for those inputs if that matters
to you.
A loader implements ImageBitmapLoader<T> from the kmpalette-loader module:
public interface ImageBitmapLoader<T> {
public suspend fun load(input: T): ImageBitmap
}core ships with these loaders:
| Loader | Input | Availability | Notes |
|---|---|---|---|
ByteArrayLoader |
ByteArray |
All | Was extensions-bytearray in 3.x. |
PainterLoader |
Painter |
All | Use rememberPainterLoader(). |
ResourceLoader |
DrawableResource |
All | Use rememberResourceLoader(). |
DrawableLoader |
@DrawableRes Int |
Android only | Use rememberDrawableLoader(). |
And these are available as separate artifacts:
| Artifact | Library | Loader | Input |
|---|---|---|---|
extensions-base64 |
N/A | Base64Loader |
String |
extensions-network |
Ktor | NetworkLoader |
Url |
extensions-file |
FileKit | PlatformFileLoader |
PlatformFile |
extensions-file |
N/A | FileLoader |
java.io.File (Android only) |
Every loader has a matching pair of composables:
| Input | Dominant color | Palette |
|---|---|---|
ImageBitmap |
rememberDominantColorState() |
rememberPaletteState() |
Painter |
rememberPainterDominantColorState()¹ |
rememberPainterPaletteState()¹ |
DrawableResource |
rememberResourceDominantColorState() |
rememberResourcePaletteState() |
@DrawableRes Int |
rememberDrawableDominantColorState() |
rememberDrawablePaletteState() |
String (Base64) |
rememberBase64DominantColorState() |
rememberBase64PaletteState() |
Url |
rememberNetworkDominantColorState() |
rememberNetworkPaletteState() |
PlatformFile |
rememberPlatformFileDominantColorState() |
rememberPlatformFilePaletteState() |
java.io.File |
rememberFileDominantColorState() |
rememberFilePaletteState() |
¹ In com.kmpalette.extensions.painter, matching the resource and drawable composables. All
dominant-color functions require defaultColor and defaultOnColor.
For example, the extensions-network module:
@Composable
fun SomeComposable(url: Url) {
val dominantColorState = rememberNetworkDominantColorState(
defaultColor = MaterialTheme.colorScheme.primary,
defaultOnColor = MaterialTheme.colorScheme.onPrimary,
)
LaunchedEffect(url) {
dominantColorState.updateFrom(url)
}
Box(
modifier = Modifier
.width(200.dp)
.height(100.dp)
.background(dominantColorState.color)
) {
Text("Some Text", color = dominantColorState.onColor)
}
}You can always fall back to the generic state functions and pass a loader yourself:
val paletteState = rememberPaletteState(loader = ByteArrayLoader)
LaunchedEffect(bytes) {
paletteState.generate(bytes)
}To generate a palette from a DrawableResource use rememberResourceDominantColorState or
rememberResourcePaletteState, which read the resource bytes directly:
@Composable
fun MyComposable() {
val dominantColorState = rememberResourceDominantColorState(
defaultColor = MaterialTheme.colorScheme.primary,
defaultOnColor = MaterialTheme.colorScheme.onPrimary,
)
LaunchedEffect(Unit) {
dominantColorState.updateFrom(Res.drawable.my_image)
}
}Alternatively, you can use the @Composable imageResource() to get an ImageBitmap then pass that
to the default loader:
@Composable
fun MyComposable() {
val image = imageResource(Res.drawable.my_image)
val dominantColorState = rememberDominantColorState(
defaultColor = Color.Black,
defaultOnColor = Color.White,
)
LaunchedEffect(image) {
dominantColorState.updateFrom(image)
}
}If you are migrating from version 3.x, please see the Migration Guide for detailed instructions on updating your code.
Key changes in 4.0:
com.materialkolor.palette, and kmpalette-core is now core
androidx-palette no longer depends on Compose; Palette.Builder takes IntArray pixelskmpalette-bitmap-loader was renamed to kmpalette-loader and is published as loader
extensions-bytearray and extensions-resources have been folded into core
extensions-libres has been removed with no replacementextensions-file now uses FileKit instead of Okiocom.kmpalette.extensions.painter
defaultColor and defaultOnColor are required; nothing depends on Material 3macosX64 and iosX64 targets were removedIf you have a feature request, please open an issue. If you would like to implement a feature request, refer to the Contributing section.
Contributions are always welcome! If you'd like to contribute, open a PR or an issue.
The module androidx-palette is licensed under the Apache License, Version 2.0. See
their LICENSE and their
repository here for more
information.
IntArray pixel data insteadRect-based region selection with plain Int coordinatesFor the remaining code see LICENSE for more information.