Skip to content

Commit

Permalink
Added support to KMP
Browse files Browse the repository at this point in the history
  • Loading branch information
extmkv committed Sep 10, 2024
1 parent 9d7a7e5 commit f843080
Show file tree
Hide file tree
Showing 53 changed files with 145 additions and 104 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ plugins {
alias(libs.plugins.kotlin) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.mavenPublish)
}

Expand Down
37 changes: 23 additions & 14 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
[versions]
ksp = "1.9.23-1.0.19"
appcompat = "1.6.1"
koin = "3.5.3"
ksp = "2.0.20-1.0.25"
appcompat = "1.7.0"
koin = "3.5.6"
koin-annotations = "1.3.1"
koin-android = "3.5.3"
koin-compose = "3.5.3"
kotlin = "1.9.23"
coroutines = "1.8.0"
kotest = "5.8.1"
material = "1.6.5"
koin-android = "3.5.6"
koin-compose = "3.5.6"
kotlin = "2.0.0"
coroutines = "1.8.1"
kotest = "5.9.0"
material = "1.7.0"
mvi = "1.7.0"
mvi-compose = "0.0.3"
activity = "1.8.2"
lifecycle = "2.7.0"
activity = "1.9.2"
lifecycle = "2.8.5"
ktlint-gradle = "12.1.0"
android-library = "8.3.1"
android-library = "8.3.2"
maven-publish = "0.25.2"
ui = "1.6.5"
mockk = "1.13.10"
ui = "1.7.0"
mockk = "1.13.11"
mvi-kotest = "0.0.2"

# its beeing used outside this file
ktlint-lib = "1.2.1"

[libraries]
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
kotestRunner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotlinBom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
mvi = { module = "com.adidas.mvi:mvi", version.ref = "mvi" }
mviCompose = { module = "com.adidas.mvi:mvi-compose", version.ref = "mvi-compose" }
Expand All @@ -49,3 +52,9 @@ mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publi
android-library = { id = "com.android.library", version.ref = "android-library" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotest = { id = "io.kotest.multiplatform", version.ref = "kotest" }
atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version = "0.25.0" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }


5 changes: 1 addition & 4 deletions mvi-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.mavenPublish)
alias(libs.plugins.compose.compiler)
}

kotlin {
Expand Down Expand Up @@ -39,10 +40,6 @@ android {
buildFeatures {
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.11"
}
}

dependencies {
Expand Down
5 changes: 1 addition & 4 deletions mvi-sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
id("kotlin-android")
id("kotlin-kapt")
alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler)
}

android {
Expand Down Expand Up @@ -37,10 +38,6 @@ android {
jvmTarget = JavaVersion.VERSION_17.toString()
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.11"
}

buildFeatures {
compose = true
}
Expand Down
53 changes: 39 additions & 14 deletions mvi/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,31 +1,56 @@
import org.jlleitschuh.gradle.ktlint.KtlintExtension

plugins {
kotlin("jvm") version libs.versions.kotlin.get()
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.ktlint)
alias(libs.plugins.mavenPublish)
alias(libs.plugins.kotest)
alias(libs.plugins.atomicfu)
}

kotlin {
explicitApi()
}

val compileTestKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks
compileTestKotlin.kotlinOptions {
freeCompilerArgs += "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi"
jvm {
withJava()
}
iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()
macosArm64()
watchosArm32()
watchosArm64()
watchosDeviceArm64()
watchosSimulatorArm64()
watchosX64()
tvosArm64()
tvosSimulatorArm64()
tvosX64()

sourceSets {
commonMain.dependencies {
implementation(libs.coroutinesCore)
}

commonTest.dependencies {
implementation(libs.kotest.framework.engine)
implementation(libs.kotest.assertions)
implementation(libs.coroutinesTest)
}

jvmTest.dependencies {
implementation(libs.kotestRunner)
}
}
}

configure<KtlintExtension> {
version.set(libs.versions.ktlint.lib.get())
}

tasks.getByName<Test>("test") {
useJUnitPlatform()
}

dependencies {
implementation(libs.coroutinesCore)

testImplementation(libs.kotestRunner)
testImplementation(libs.coroutinesTest)
tasks {
withType<Test> {
useJUnitPlatform()
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.adidas.mvi

import java.util.Collections
import kotlin.reflect.KClass

internal class Multimap<TKey : Any, TValue> {
private val innerMap = hashMapOf<TKey, MutableList<MultimapEntry<TKey, TValue>>>()

val keys: Collection<TKey>
get() = Collections.unmodifiableSet(innerMap.keys)
get() = innerMap.keys.toSet()

fun put(
key: TKey,
Expand All @@ -29,7 +28,8 @@ internal class Multimap<TKey : Any, TValue> {
innerMap[key] = MutableList(values.size) { values[it] }
}

operator fun get(key: TKey): List<MultimapEntry<TKey, TValue>> = innerMap[key]?.let(Collections::unmodifiableList) ?: listOf()
operator fun get(key: TKey): List<MultimapEntry<TKey, TValue>> =
innerMap[key]?.toList() ?: listOf()

operator fun <T : TKey> get(keyClass: KClass<T>): List<MultimapEntry<TKey, TValue>> =
keys
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package com.adidas.mvi

import kotlinx.coroutines.CancellationException

internal class TerminatedIntentException : CancellationException()
internal class TerminatedIntentException : CancellationException("Terminated intent")
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public fun <TIntent : Intent, TInnerState : LoggableState, TAction> Reducer(
@Suppress("UNCHECKED_CAST")
public inline fun <reified TView : Any> Reducer<*, *>.requireView(): TView =
(state.value as State<TView, *>).view.apply {
if (this.javaClass != TView::class.java) {
throw ClassCastException("Required view of ${TView::class.java} type, but found ${this.javaClass}")
if (!TView::class.isInstance(this)) {
throw ClassCastException("Required view of ${TView::class} type, but found ${this}")
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
package com.adidas.mvi.sideeffects

import java.util.LinkedList
import java.util.Queue
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic

/**
* A thread-safe side effect container
* It only returns each SideEffect once, if you use it as an [Iterable] it will emit each SideEffect and remove them so it's a perfect case for one shot SideEffects.
* It locks itself, so you can't add and read at the same time, also it's not possible to read it at the same time from different threads, being completely thread-safe.
*/

public class SideEffects<T>() : Iterable<T> {
private val sideEffects: Queue<T> = LinkedList()
private val sideEffects: AtomicRef<MutableList<T>> = atomic(ArrayList())

// Private constructor to initialize from an Iterable
private constructor(sideEffects: Iterable<T>) : this() {
this.sideEffects.addAll(sideEffects)
this.sideEffects.value.addAll(sideEffects)
}

public fun add(vararg sideEffectsToAdd: T): SideEffects<T> {
return SideEffects(sideEffects + sideEffectsToAdd)
val newList = sideEffects.value.toMutableList()
newList.addAll(sideEffectsToAdd)
return SideEffects(newList)
}

public fun clear(): SideEffects<T> {
return SideEffects()
}

override fun iterator(): Iterator<T> =
iterator {
do {
val nextSideEffect: T? = sideEffects.poll()
nextSideEffect?.let { yield(it) }
} while (nextSideEffect != null)
override fun iterator(): Iterator<T> = iterator {
while (true) {
val currentList = sideEffects.value
if (currentList.isEmpty()) break
val nextSideEffect = currentList.removeFirstOrNull()
nextSideEffect?.let { yield(it) }
}
}
}
}
35 changes: 35 additions & 0 deletions mvi/src/commonTest/kotlin/com/adidas/mvi/CoroutineListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.adidas.mvi

import io.kotest.core.listeners.TestListener
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain

internal class CoroutineListener(
internal val testCoroutineDispatcher: TestDispatcher = UnconfinedTestDispatcher(
TestCoroutineScheduler()
),
) : TestListener {

internal val dispatchersContainer: DispatchersContainer =
FixedDispatchersContainer(testCoroutineDispatcher)

override suspend fun beforeContainer(testCase: TestCase) {
Dispatchers.setMain(testCoroutineDispatcher)
}

override suspend fun afterContainer(testCase: TestCase, result: TestResult) {
Dispatchers.resetMain()
testCoroutineDispatcher.scheduler.cancel()
}

public fun advanceUntilIdle() {
testCoroutineDispatcher.scheduler.advanceUntilIdle()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,4 @@ internal class MultimapTests : BehaviorSpec({
}
}
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ private fun createIntentExecutorContainer(
}

private fun createIntentExecutorContainer(
exception: java.lang.Exception,
exception: Throwable,
): (TestIntent) -> Flow<StateTransform<State<TestState, TestSideEffect>>> =
{
if (it is TestIntent.SimpleIntent) throw exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.adidas.mvi.reducer.logger

import com.adidas.mvi.Loggable
import com.adidas.mvi.Logger
import java.lang.StringBuilder

private const val SPACE = " "
internal const val SUCCESSFUL_INTENT = "SuccessfulIntent:"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldContainInOrder
import java.util.concurrent.Semaphore
import kotlin.concurrent.thread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import kotlin.time.toDuration
Expand Down Expand Up @@ -66,33 +67,31 @@ internal class SideEffectsTest : BehaviorSpec({

var returnedSideEffects = sideEffects.add(firstSideEffect)

val semaphore = Semaphore(0)
val semaphore = Semaphore(2)

val readThread =
thread {
returnedSideEffects.forEach { _ ->
semaphore.acquire()
}
val readJob = launch(Dispatchers.Default) {
returnedSideEffects.forEach { _ ->
semaphore.acquire() // Wait for the signal
}
}

val addThread =
thread {
returnedSideEffects = returnedSideEffects.add(secondSideEffectToBeAddedLater)
}
val addJob = launch(Dispatchers.Default) {
returnedSideEffects = sideEffects.add(secondSideEffectToBeAddedLater)
}

semaphore.release()

then("It should be released only by the semaphore").config(
timeout =
5.toDuration(
DurationUnit.SECONDS,
),
5.toDuration(
DurationUnit.SECONDS,
),
) {
readThread.join()
addThread.join()
readJob.join()
addJob.join()

readThread.isAlive.shouldBeFalse()
addThread.isAlive.shouldBeFalse()
readJob.isActive.shouldBeFalse()
addJob.isActive.shouldBeFalse()
returnedSideEffects.shouldContain(secondSideEffectToBeAddedLater)
}
}
Expand Down
Loading

0 comments on commit f843080

Please sign in to comment.