-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Compose color provider #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
MessiasLima
merged 2 commits into
main
from
81-implement-compose-graphics-resolvers-color-colorstrategy-solidcolor
Jul 6, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
android/src/main/java/dev/appoutlet/some/android/resolver/compose/ColorResolver.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package dev.appoutlet.some.android.resolver.compose | ||
|
|
||
| import androidx.compose.ui.graphics.Color | ||
| import dev.appoutlet.some.android.strategy.ColorStrategy | ||
| import dev.appoutlet.some.core.Resolver | ||
| import dev.appoutlet.some.core.ResolverChain | ||
| import dev.appoutlet.some.core.StrategyProvider | ||
| import dev.appoutlet.some.core.get | ||
| import kotlin.random.Random | ||
| import kotlin.reflect.KType | ||
| import kotlin.reflect.typeOf | ||
|
|
||
| private const val HUE_MAX = 360f | ||
|
|
||
| /** | ||
| * Resolves Compose [Color] values using the active [ColorStrategy]. | ||
| * | ||
| * @param strategyProvider Provider of all configured generation strategies. | ||
| * @param random Random source used when generating channel values. | ||
| */ | ||
| class ColorResolver( | ||
| strategyProvider: StrategyProvider, | ||
| private val random: Random | ||
| ) : Resolver { | ||
| private val colorStrategy = strategyProvider.get<ColorStrategy>() ?: ColorStrategy.default | ||
|
|
||
| override fun canResolve(type: KType): Boolean = type == typeOf<Color>() | ||
|
|
||
| override fun resolve(type: KType, chain: ResolverChain): Any { | ||
| return when (colorStrategy) { | ||
| is ColorStrategy.RandomArgb -> resolveRandomArgb() | ||
| is ColorStrategy.RandomHsl -> resolveRandomHsl() | ||
| is ColorStrategy.Palette -> colorStrategy.colors.random(random) | ||
| } | ||
| } | ||
|
|
||
| private fun resolveRandomArgb(): Color { | ||
| return Color( | ||
| red = random.nextFloat(), | ||
| green = random.nextFloat(), | ||
| blue = random.nextFloat(), | ||
| alpha = random.nextFloat() | ||
| ) | ||
| } | ||
|
|
||
| private fun resolveRandomHsl(): Color { | ||
| val hue = random.nextFloat() * HUE_MAX | ||
| val saturation = random.nextFloat() | ||
| val lightness = random.nextFloat() | ||
| val alpha = random.nextFloat() | ||
|
|
||
| return Color.hsl( | ||
| hue = hue, | ||
| saturation = saturation, | ||
| lightness = lightness, | ||
| alpha = alpha | ||
| ) | ||
| } | ||
| } |
51 changes: 51 additions & 0 deletions
51
android/src/main/java/dev/appoutlet/some/android/strategy/ColorStrategy.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package dev.appoutlet.some.android.strategy | ||
|
|
||
| import androidx.compose.ui.graphics.Color | ||
| import dev.appoutlet.some.config.Strategy | ||
|
|
||
| /** | ||
| * Strategy for generating Compose [Color] values. | ||
| * | ||
| * Available strategies: | ||
| * - [RandomArgb] - generates fully random red, green, blue, and alpha channels (default) | ||
| * - [RandomHsl] - generates colors from random hue, saturation, and lightness values | ||
| * - [Palette] - chooses from a fixed list of colors | ||
| * | ||
| * Example usage: | ||
| * ```kotlin | ||
| * someSetup { | ||
| * strategy(ColorStrategy.RandomHsl) | ||
| * } | ||
| * ``` | ||
| */ | ||
| sealed interface ColorStrategy : Strategy { | ||
| override val key get() = ColorStrategy::class | ||
|
|
||
| /** | ||
| * Generates Compose colors with fully random ARGB channels. | ||
| */ | ||
| data object RandomArgb : ColorStrategy | ||
|
|
||
| /** | ||
| * Generates Compose colors from random HSL values. | ||
| */ | ||
| data object RandomHsl : ColorStrategy | ||
|
|
||
| /** | ||
| * Chooses colors from a fixed palette. | ||
| * | ||
| * @param colors Palette values eligible for generation. | ||
| */ | ||
| data class Palette(val colors: List<Color>) : ColorStrategy { | ||
| init { | ||
| require(colors.isNotEmpty()) { "Palette must contain at least one color" } | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| /** | ||
| * The default Compose color strategy. | ||
| */ | ||
| val default: ColorStrategy get() = RandomArgb | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
android/src/test/java/dev/appoutlet/some/android/resolver/compose/ColorResolverTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| package dev.appoutlet.some.android.resolver.compose | ||
|
|
||
| import androidx.compose.ui.graphics.Color | ||
| import dev.appoutlet.some.android.strategy.ColorStrategy | ||
| import dev.appoutlet.some.config.DefaultStrategyProvider | ||
| import dev.appoutlet.some.config.NullableStrategy | ||
| import dev.appoutlet.some.core.ResolverChain | ||
| import org.junit.Assert.assertEquals | ||
| import org.junit.Assert.assertFalse | ||
| import org.junit.Assert.assertTrue | ||
| import org.junit.Test | ||
| import kotlin.random.Random | ||
| import kotlin.reflect.typeOf | ||
| import kotlin.test.assertNotNull | ||
|
|
||
| class ColorResolverTest { | ||
| @Test | ||
| fun `ColorResolver generates Compose Color values in default RandomArgb mode`() { | ||
| val resolver = ColorResolver(DefaultStrategyProvider(), FloatSequenceRandom(0.1f, 0.2f, 0.3f, 0.4f)) | ||
|
|
||
| val result = resolver.resolve(typeOf<Color>(), testChain) as Color | ||
|
|
||
| assertEquals(Color(0.1f, 0.2f, 0.3f, 0.4f), result) | ||
| } | ||
|
|
||
| @Test | ||
| fun `ColorResolver RandomHsl produces HSL converted colors`() { | ||
| val resolver = ColorResolver( | ||
| DefaultStrategyProvider(mapOf(ColorStrategy::class to ColorStrategy.RandomHsl)), | ||
| Random.Default | ||
| ) | ||
|
|
||
| val result = resolver.resolve(typeOf<Color>(), testChain) as Color | ||
|
|
||
| assertNotNull(result) | ||
| } | ||
|
|
||
| @Test | ||
| fun `ColorResolver Palette only returns configured colors`() { | ||
| val expected = Color(0.3f, 0.4f, 0.5f, 0.6f) | ||
| val resolver = ColorResolver( | ||
| DefaultStrategyProvider(mapOf(ColorStrategy::class to ColorStrategy.Palette(listOf(expected)))), | ||
| Random.Default | ||
| ) | ||
|
|
||
| repeat(10) { | ||
| val result = resolver.resolve(typeOf<Color>(), testChain) as Color | ||
| assertEquals(expected, result) | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `ColorResolver canResolve detects Compose Color type`() { | ||
| val resolver = ColorResolver(DefaultStrategyProvider(), Random.Default) | ||
| assertTrue(resolver.canResolve(typeOf<Color>())) | ||
| } | ||
|
|
||
| @Test | ||
| fun `ColorResolver rejects non-Compose Color types`() { | ||
| val resolver = ColorResolver(DefaultStrategyProvider(), Random.Default) | ||
| assertFalse(resolver.canResolve(typeOf<String>())) | ||
| assertFalse(resolver.canResolve(typeOf<Int>())) | ||
| assertFalse(resolver.canResolve(typeOf<android.graphics.Color>())) | ||
| } | ||
|
|
||
| private class FloatSequenceRandom(vararg values: Float) : Random() { | ||
| private val values = values.toList() | ||
| private var index = 0 | ||
|
|
||
| override fun nextBits(bitCount: Int): Int = 0 | ||
|
|
||
| override fun nextFloat(): Float { | ||
| return values.getOrElse(index++) { error("No more float values configured for test random") } | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| private val testChain = ResolverChain(emptyList(), NullableStrategy.NullOnCircularReference) | ||
| } | ||
| } | ||
17 changes: 17 additions & 0 deletions
17
android/src/test/java/dev/appoutlet/some/android/strategy/ColorStrategyTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package dev.appoutlet.some.android.strategy | ||
|
|
||
| import androidx.compose.ui.graphics.Color | ||
| import org.junit.Assert.assertEquals | ||
| import org.junit.Test | ||
|
|
||
| class ColorStrategyTest { | ||
| @Test | ||
| fun `ColorStrategy default is RandomArgb`() { | ||
| assertEquals(ColorStrategy.RandomArgb, ColorStrategy.default) | ||
| } | ||
|
|
||
| @Test(expected = IllegalArgumentException::class) | ||
| fun `ColorStrategy Palette rejects empty color lists`() { | ||
| ColorStrategy.Palette(emptyList<Color>()) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| --- | ||
| icon: lucide/palette | ||
| --- | ||
|
|
||
| # Compose Color | ||
|
|
||
| `some-android` includes a resolver for `androidx.compose.ui.graphics.Color`. | ||
|
|
||
| Once `some-android` is on your test classpath, `some<ComposeColor>()` works without custom factories or custom resolvers. | ||
|
|
||
| !!! note "Android-only feature" | ||
|
|
||
| `ColorStrategy` is part of `some-android`. It is not available from `some-core`. | ||
|
|
||
| ## Basic usage | ||
|
|
||
| ```kotlin | ||
| import androidx.compose.ui.graphics.Color as ComposeColor | ||
| import dev.appoutlet.some.some | ||
|
|
||
| val color: ComposeColor = some() | ||
| ``` | ||
|
|
||
| Use an import alias like `ComposeColor` when the same file also references `android.graphics.Color`. | ||
|
|
||
| ## Color strategy | ||
|
|
||
| Use `ColorStrategy` when the distribution of generated Compose colors matters for the code you are testing. | ||
|
|
||
| | Strategy | Default | What it generates | Good for | | ||
| |----------|---------|-------------------|----------| | ||
| | `ColorStrategy.RandomArgb` | Yes | Fully random red, green, blue, and alpha channels | General UI tests that only need any valid Compose color | | ||
| | `ColorStrategy.RandomHsl` | No | Random hue, saturation, and lightness converted to a Compose color | Tests that want a more color-model-aware distribution | | ||
| | `ColorStrategy.Palette` | No | A random value chosen from a fixed list of Compose colors | Design-system palettes, brand colors, and snapshot stability | | ||
|
|
||
| ### Configure it per call | ||
|
|
||
| ```kotlin | ||
| import androidx.compose.ui.graphics.Color as ComposeColor | ||
| import dev.appoutlet.some.android.strategy.ColorStrategy | ||
| import dev.appoutlet.some.some | ||
|
|
||
| val accent = some<ComposeColor> { | ||
| strategy(ColorStrategy.RandomHsl) | ||
| } | ||
| ``` | ||
|
|
||
| ### Configure it once and reuse it | ||
|
|
||
| ```kotlin | ||
| import androidx.compose.ui.graphics.Color as ComposeColor | ||
| import dev.appoutlet.some.android.strategy.ColorStrategy | ||
| import dev.appoutlet.some.someSetup | ||
|
|
||
| val some = someSetup { | ||
| strategy( | ||
| ColorStrategy.Palette( | ||
| listOf( | ||
| ComposeColor(0.40f, 0.23f, 0.72f, 1f), | ||
| ComposeColor(0.15f, 0.61f, 0.84f, 1f), | ||
| ComposeColor(0.96f, 0.57f, 0.17f, 1f) | ||
| ) | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| val primary: ComposeColor = some() | ||
| val secondary: ComposeColor = some() | ||
| ``` | ||
|
|
||
| ## What the resolvers generate | ||
|
|
||
| - `ColorStrategy.RandomArgb` randomizes red, green, blue, and alpha independently | ||
| - `ColorStrategy.RandomHsl` converts randomized hue, saturation, and lightness values into RGB channels | ||
| - `ColorStrategy.Palette` only returns colors from the configured palette | ||
|
|
||
| ## Picking the right strategy | ||
|
|
||
| - Use `ColorStrategy.RandomArgb` when your test only needs any valid Compose color | ||
| - Use `ColorStrategy.RandomHsl` when hue or tonal distribution matters more than raw channel randomness | ||
| - Use `ColorStrategy.Palette` when your UI should stay inside a known set of allowed colors | ||
|
|
||
| ## Examples | ||
|
|
||
| === "RandomArgb (default)" | ||
|
|
||
| ```kotlin | ||
| val color: ComposeColor = some() | ||
| ``` | ||
|
|
||
| === "RandomHsl" | ||
|
|
||
| ```kotlin | ||
| val color = some<ComposeColor> { | ||
| strategy(ColorStrategy.RandomHsl) | ||
| } | ||
| ``` | ||
|
|
||
| === "Palette" | ||
|
|
||
| ```kotlin | ||
| val color = some<ComposeColor> { | ||
| strategy( | ||
| ColorStrategy.Palette( | ||
| listOf( | ||
| ComposeColor(0.12f, 0.13f, 0.18f, 1f), | ||
| ComposeColor(0.87f, 0.33f, 0.29f, 1f) | ||
| ) | ||
| ) | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| See [Android](index.md) for installation and the list of Android-specific supported types. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.