From 8926b3237b3a950ff05c45536f692c273d8ed967 Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Mon, 22 Jun 2026 14:58:18 +0200 Subject: [PATCH] new: integrate a dispatcher provider and dispatch work --- core/coroutines/build.gradle.kts | 53 +++++++++++++++++++ .../coroutines/data/IoDispatcher.android.kt | 6 +++ .../data/DefaultDispatcherProvider.kt | 11 ++++ .../core/coroutines/data/IoDispatcher.kt | 5 ++ .../core/coroutines/di/CoroutinesModule.kt | 10 ++++ .../coroutines/domain/DispatcherProvider.kt | 9 ++++ .../core/coroutines/data/IoDispatcher.ios.kt | 8 +++ .../core/coroutines/data/IoDispatcher.js.kt | 7 +++ .../core/coroutines/data/IoDispatcher.jvm.kt | 6 +++ .../coroutines/data/IoDispatcher.wasmJs.kt | 7 +++ data/subtitle/build.gradle.kts | 6 ++- .../subtitle/data/LrcSubtitleRepository.kt | 17 +++--- settings.gradle.kts | 1 + shared/build.gradle.kts | 1 + .../kotlin/fr/ajaury/gwenedeg/App.kt | 2 + .../data/ComposeResourceReader.kt | 11 +++- 16 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 core/coroutines/build.gradle.kts create mode 100644 core/coroutines/src/androidMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.android.kt create mode 100644 core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/DefaultDispatcherProvider.kt create mode 100644 core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.kt create mode 100644 core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/di/CoroutinesModule.kt create mode 100644 core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/domain/DispatcherProvider.kt create mode 100644 core/coroutines/src/iosMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.ios.kt create mode 100644 core/coroutines/src/jsMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.js.kt create mode 100644 core/coroutines/src/jvmMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.jvm.kt create mode 100644 core/coroutines/src/wasmJsMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.wasmJs.kt diff --git a/core/coroutines/build.gradle.kts b/core/coroutines/build.gradle.kts new file mode 100644 index 0000000..1d86e26 --- /dev/null +++ b/core/coroutines/build.gradle.kts @@ -0,0 +1,53 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) +} + +kotlin { + iosArm64() + iosSimulatorArm64() + + jvm() + + js { + browser() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + } + + androidLibrary { + namespace = "fr.ajaury.gwenedeg.core.coroutines" + compileSdk = + libs.versions.androidCompileSdk + .get() + .toInt() + minSdk = + libs.versions.androidMinSdk + .get() + .toInt() + + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } + } + + sourceSets { + commonMain.dependencies { + // Coroutines + implementation(libs.kotlinx.coroutinesCore) + + // DI + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} diff --git a/core/coroutines/src/androidMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.android.kt b/core/coroutines/src/androidMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.android.kt new file mode 100644 index 0000000..5d176b5 --- /dev/null +++ b/core/coroutines/src/androidMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.android.kt @@ -0,0 +1,6 @@ +package fr.ajaury.gwenedeg.core.coroutines.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +internal actual val ioDispatcher: CoroutineDispatcher = Dispatchers.IO diff --git a/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/DefaultDispatcherProvider.kt b/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/DefaultDispatcherProvider.kt new file mode 100644 index 0000000..5b0373c --- /dev/null +++ b/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/DefaultDispatcherProvider.kt @@ -0,0 +1,11 @@ +package fr.ajaury.gwenedeg.core.coroutines.data + +import fr.ajaury.gwenedeg.core.coroutines.domain.DispatcherProvider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +internal class DefaultDispatcherProvider : DispatcherProvider { + override val main: CoroutineDispatcher = Dispatchers.Main + override val default: CoroutineDispatcher = Dispatchers.Default + override val io: CoroutineDispatcher = ioDispatcher +} diff --git a/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.kt b/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.kt new file mode 100644 index 0000000..d37fd9d --- /dev/null +++ b/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.kt @@ -0,0 +1,5 @@ +package fr.ajaury.gwenedeg.core.coroutines.data + +import kotlinx.coroutines.CoroutineDispatcher + +internal expect val ioDispatcher: CoroutineDispatcher diff --git a/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/di/CoroutinesModule.kt b/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/di/CoroutinesModule.kt new file mode 100644 index 0000000..65ec3f3 --- /dev/null +++ b/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/di/CoroutinesModule.kt @@ -0,0 +1,10 @@ +package fr.ajaury.gwenedeg.core.coroutines.di + +import fr.ajaury.gwenedeg.core.coroutines.data.DefaultDispatcherProvider +import fr.ajaury.gwenedeg.core.coroutines.domain.DispatcherProvider +import org.koin.core.module.Module +import org.koin.dsl.module + +val coroutinesModule: Module = module { + single { DefaultDispatcherProvider() } +} diff --git a/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/domain/DispatcherProvider.kt b/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/domain/DispatcherProvider.kt new file mode 100644 index 0000000..4d57c4b --- /dev/null +++ b/core/coroutines/src/commonMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/domain/DispatcherProvider.kt @@ -0,0 +1,9 @@ +package fr.ajaury.gwenedeg.core.coroutines.domain + +import kotlinx.coroutines.CoroutineDispatcher + +interface DispatcherProvider { + val main: CoroutineDispatcher + val default: CoroutineDispatcher + val io: CoroutineDispatcher +} diff --git a/core/coroutines/src/iosMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.ios.kt b/core/coroutines/src/iosMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.ios.kt new file mode 100644 index 0000000..78b8c61 --- /dev/null +++ b/core/coroutines/src/iosMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.ios.kt @@ -0,0 +1,8 @@ +package fr.ajaury.gwenedeg.core.coroutines.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +// `Dispatchers.IO` is not public API on Native; `Dispatchers.Default` is multi-threaded here, so it +// still runs work off the main thread. +internal actual val ioDispatcher: CoroutineDispatcher = Dispatchers.Default diff --git a/core/coroutines/src/jsMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.js.kt b/core/coroutines/src/jsMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.js.kt new file mode 100644 index 0000000..2583e70 --- /dev/null +++ b/core/coroutines/src/jsMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.js.kt @@ -0,0 +1,7 @@ +package fr.ajaury.gwenedeg.core.coroutines.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +// JS is single-threaded; there is no dedicated I/O dispatcher, use default. +internal actual val ioDispatcher: CoroutineDispatcher = Dispatchers.Default diff --git a/core/coroutines/src/jvmMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.jvm.kt b/core/coroutines/src/jvmMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.jvm.kt new file mode 100644 index 0000000..5d176b5 --- /dev/null +++ b/core/coroutines/src/jvmMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.jvm.kt @@ -0,0 +1,6 @@ +package fr.ajaury.gwenedeg.core.coroutines.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +internal actual val ioDispatcher: CoroutineDispatcher = Dispatchers.IO diff --git a/core/coroutines/src/wasmJsMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.wasmJs.kt b/core/coroutines/src/wasmJsMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.wasmJs.kt new file mode 100644 index 0000000..94ab444 --- /dev/null +++ b/core/coroutines/src/wasmJsMain/kotlin/fr/ajaury/gwenedeg/core/coroutines/data/IoDispatcher.wasmJs.kt @@ -0,0 +1,7 @@ +package fr.ajaury.gwenedeg.core.coroutines.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +// Wasm is single-threaded; there is no dedicated I/O dispatcher, use default +internal actual val ioDispatcher: CoroutineDispatcher = Dispatchers.Default diff --git a/data/subtitle/build.gradle.kts b/data/subtitle/build.gradle.kts index 85a7630..a5da309 100644 --- a/data/subtitle/build.gradle.kts +++ b/data/subtitle/build.gradle.kts @@ -39,9 +39,13 @@ kotlin { sourceSets { commonMain.dependencies { - // Logging + // Core modules + implementation(projects.core.coroutines) implementation(projects.core.logging) + // Coroutines + implementation(libs.kotlinx.coroutinesCore) + // DI implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.core) diff --git a/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt index 84f6d27..a9e438f 100644 --- a/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt +++ b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt @@ -1,20 +1,25 @@ package fr.ajaury.gwenedeg.subtitle.data +import fr.ajaury.gwenedeg.core.coroutines.domain.DispatcherProvider import fr.ajaury.gwenedeg.core.logging.domain.Logger import fr.ajaury.gwenedeg.subtitle.domain.Subtitle import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository import fr.ajaury.gwenedeg.subtitle.model.CantGetSubtitleException +import kotlinx.coroutines.withContext internal class LrcSubtitleRepository( private val parser: LrcParser, + private val dispatcherProvider: DispatcherProvider, private val logger: Logger, ) : SubtitleRepository { override suspend fun getSubtitle(content: String): Subtitle = - try { - parser.parse(content = content) - } catch (exception: Exception) { - val message = "Failed to load subtitle: $content" - logger.error(message = message, throwable = exception) - throw CantGetSubtitleException(message) + withContext(dispatcherProvider.default) { + try { + parser.parse(content = content) + } catch (exception: Exception) { + val message = "Failed to load subtitle: $content" + logger.error(message = message, throwable = exception) + throw CantGetSubtitleException(message) + } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index f0c9e08..a03c15a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,6 +39,7 @@ include(":shared") // Core modules include(":core:audioplayer") +include(":core:coroutines") include(":core:logging") // Data modules diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 57d3a15..5bc6db6 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -60,6 +60,7 @@ kotlin { commonMain.dependencies { // Core modules implementation(projects.core.audioplayer) + implementation(projects.core.coroutines) implementation(projects.core.logging) // Data modules diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt index 84a0cc8..2ff69e3 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.navigation3.runtime.NavEntry import androidx.navigation3.ui.NavDisplay import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSerializer +import fr.ajaury.gwenedeg.core.coroutines.di.coroutinesModule import fr.ajaury.gwenedeg.core.logging.di.loggingModule import fr.ajaury.gwenedeg.di.sharedModule import fr.ajaury.gwenedeg.navigation.Route @@ -26,6 +27,7 @@ fun App() { modules( sharedModule, audioPlayerModule, + coroutinesModule, loggingModule, subtitleModule ) diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/data/ComposeResourceReader.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/data/ComposeResourceReader.kt index aa60731..f934df9 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/data/ComposeResourceReader.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/data/ComposeResourceReader.kt @@ -1,8 +1,15 @@ package fr.ajaury.gwenedeg.resourcereader.data +import fr.ajaury.gwenedeg.core.coroutines.domain.DispatcherProvider import fr.ajaury.gwenedeg.resourcereader.data.domain.ResourceReader import gwenedeg.shared.generated.resources.Res +import kotlinx.coroutines.withContext -internal class ComposeResourceReader : ResourceReader { - override suspend fun read(resourcePath: String): String = Res.readBytes(resourcePath).decodeToString() +internal class ComposeResourceReader( + private val dispatcherProvider: DispatcherProvider, +) : ResourceReader { + override suspend fun read(resourcePath: String): String = + withContext(dispatcherProvider.io) { + Res.readBytes(resourcePath).decodeToString() + } }