Skip to content

Commit

Permalink
Add suppor to KMP to the library MVI (#29)
Browse files Browse the repository at this point in the history
* Added support to KMP

* Bumped MVI to 1.8.0
  • Loading branch information
extmkv authored Sep 10, 2024
1 parent 9d7a7e5 commit 0b17235
Show file tree
Hide file tree
Showing 55 changed files with 143 additions and 98 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
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img style='width: 500px' src="assets/mvi_logo.png" alt="adidas mvi Logo"/>
</div>

[![Kotlin](https://img.shields.io/badge/Kotlin-1.9.22-blue.svg?style=flat&logo=kotlin)](https://kotlinlang.org)
[![Kotlin](https://img.shields.io/badge/Kotlin-2.0.0-blue.svg?style=flat&logo=kotlin)](https://kotlinlang.org)
![Test workflow](https://github.com/adidas/mvi/actions/workflows/deploy_docs.yml/badge.svg)
[![adidas official](https://img.shields.io/badge/adidas-official-000000)](https://github.com/adidas)

Expand Down
39 changes: 24 additions & 15 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"
maven-publish = "0.25.2"
ui = "1.6.5"
mockk = "1.13.10"
android-library = "8.3.2"
maven-publish = "0.29.0"
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
54 changes: 40 additions & 14 deletions mvi/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,31 +1,57 @@
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)
}

jvmTest.dependencies {
implementation(libs.kotest.framework.engine)
implementation(libs.kotest.assertions)
implementation(libs.coroutinesTest)
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()
}
}
2 changes: 1 addition & 1 deletion mvi/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
POM_ARTIFACT_ID=mvi
GROUP=com.adidas.mvi
VERSION_CODE=1
VERSION_NAME=1.7.0
VERSION_NAME=1.8.0
POM_NAME=Adidas MVI
POM_DESCRIPTION=Adidas MVI
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,7 @@ 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,22 +1,26 @@
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> {
Expand All @@ -25,9 +29,11 @@ public class SideEffects<T>() : Iterable<T> {

override fun iterator(): Iterator<T> =
iterator {
do {
val nextSideEffect: T? = sideEffects.poll()
while (true) {
val currentList = sideEffects.value
if (currentList.isEmpty()) break
val nextSideEffect = currentList.removeFirstOrNull()
nextSideEffect?.let { yield(it) }
} while (nextSideEffect != null)
}
}
}
38 changes: 38 additions & 0 deletions mvi/src/jvmTest/kotlin/com/adidas/mvi/CoroutineListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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 @@ -197,9 +197,7 @@ private fun createIntentExecutorContainer(
flowOf(TestTransform.Transform1)
}

private fun createIntentExecutorContainer(
exception: java.lang.Exception,
): (TestIntent) -> Flow<StateTransform<State<TestState, TestSideEffect>>> =
private fun createIntentExecutorContainer(exception: Throwable): (TestIntent) -> Flow<StateTransform<State<TestState, TestSideEffect>>> =
{
if (it is TestIntent.SimpleIntent) throw exception
emptyFlow()
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,18 +67,18 @@ internal class SideEffectsTest : BehaviorSpec({

var returnedSideEffects = sideEffects.add(firstSideEffect)

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

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

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

semaphore.release()
Expand All @@ -88,11 +89,11 @@ internal class SideEffectsTest : BehaviorSpec({
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 0b17235

Please sign in to comment.