diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d4e071..2e8ad34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.1.0] - 2026-01-16 + +### ๐ŸŽ‰ Major Enhancement: DI-Agnostic Architecture + +KMP WorkManager is now **dependency injection framework agnostic**! Koin is optional. + +### Added + +#### Core Library (Zero DI Dependencies) +- **WorkerManagerConfig**: Global service locator for DI-agnostic factory registration +- **WorkerManagerInitializer**: Unified initialization API (expect/actual pattern) + - Manual initialization without any DI framework + - Platform-specific setup (Android Context, iOS task IDs) +- **AndroidWorkerFactoryProvider** / **IosWorkerFactoryProvider**: Type-safe factory accessors +- **IosTaskHandlerRegistry**: Lazy-initialized task executors for iOS + +#### Extension Modules +- **kmpworkmanager-koin** (v2.1.0): Optional Koin integration extension + - 100% backward compatible with v2.0.0 + - Same API, just add the dependency +- **kmpworkmanager-hilt** (v2.1.0): Optional Hilt/Dagger integration (Android only) + - Native Hilt support for Android apps + +### Changed + +- **Core library**: Removed Koin dependencies (koin-core, koin-android) +- **KmpWorker** / **KmpHeavyWorker**: Use `AndroidWorkerFactoryProvider` instead of Koin injection +- **Version**: 2.0.0 โ†’ 2.1.0 (minor version bump - non-breaking) + +### Deprecated + +- **KoinModule files** in core library: Moved to `kmpworkmanager-koin` extension + - Old code still works with extension dependency + - Will be removed in v3.0.0 + +### Migration + +**For existing Koin users** (100% backward compatible): +```kotlin +// Just add one dependency - code stays the same +implementation("dev.brewkits:kmpworkmanager:2.1.0") +implementation("dev.brewkits:kmpworkmanager-koin:2.1.0") // ADD THIS +``` + +**For new projects** (manual initialization): +```kotlin +// Android +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + WorkerManagerInitializer.initialize( + workerFactory = MyWorkerFactory(), + context = this + ) + } +} + +// iOS +fun initializeWorkManager() { + WorkerManagerInitializer.initialize( + workerFactory = MyWorkerFactory(), + iosTaskIds = setOf("my-task") + ) +} +``` + +See [docs/migration-v2.1.0.md](docs/migration-v2.1.0.md) for complete migration guide. + +### Benefits + +- ๐ŸŽฏ **Zero dependencies**: Core library has no DI framework requirements +- ๐Ÿ”Œ **Flexible integration**: Choose your DI solution (Koin, Hilt, manual, or others) +- ๐Ÿ“ฆ **Smaller binary size**: Only include DI framework if you need it +- ๐Ÿงช **Easier testing**: Simple manual initialization for tests +- โ™ป๏ธ **Backward compatible**: Existing Koin code works with extension module + +--- + ## [2.0.0] - 2026-01-15 ### BREAKING CHANGES diff --git a/README.md b/README.md index 82343bc..cf907b3 100644 --- a/README.md +++ b/README.md @@ -92,26 +92,52 @@ The library handles platform-specific details automatically. ## Installation -Add to your `build.gradle.kts`: +**Version 2.1.0+**: Choose your dependency injection approach: + +### Option 1: Manual (No DI Framework) - Recommended for New Projects + +```kotlin +kotlin { + sourceSets { + commonMain.dependencies { + implementation("dev.brewkits:kmpworkmanager:2.1.0") + } + } +} +``` + +### Option 2: Koin Integration - For Existing Koin Users ```kotlin kotlin { sourceSets { commonMain.dependencies { - implementation("dev.brewkits:kmpworkmanager:2.0.0") + implementation("dev.brewkits:kmpworkmanager:2.1.0") + implementation("dev.brewkits:kmpworkmanager-koin:2.1.0") } } } ``` -Or using version catalog: +### Option 3: Hilt Integration (Android Only) - For Hilt/Dagger Users + +```kotlin +dependencies { + implementation("dev.brewkits:kmpworkmanager:2.1.0") + implementation("dev.brewkits:kmpworkmanager-hilt:2.1.0") +} +``` + +**Version Catalog** (recommended): ```toml [versions] -kmpworkmanager = "2.0.0" +kmpworkmanager = "2.1.0" [libraries] kmpworkmanager = { module = "dev.brewkits:kmpworkmanager", version.ref = "kmpworkmanager" } +kmpworkmanager-koin = { module = "dev.brewkits:kmpworkmanager-koin", version.ref = "kmpworkmanager" } +kmpworkmanager-hilt = { module = "dev.brewkits:kmpworkmanager-hilt", version.ref = "kmpworkmanager" } ``` ## Quick Start @@ -170,7 +196,36 @@ class MyWorkerFactory : IosWorkerFactory { } ``` -### 3. Initialize Koin +### 3. Initialize the Library + +#### Option A: Manual Initialization (No DI Framework) + +**Android** (`Application.kt`): + +```kotlin +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + WorkerManagerInitializer.initialize( + workerFactory = MyWorkerFactory(), + context = this + ) + } +} +``` + +**iOS** (call from `AppDelegate`): + +```kotlin +fun initializeWorkManager() { + WorkerManagerInitializer.initialize( + workerFactory = MyWorkerFactory(), + iosTaskIds = setOf("kmp-sync-task", "kmp-upload-task") + ) +} +``` + +#### Option B: With Koin **Android** (`Application.kt`): diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 6cd36e5..10bc636 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -45,8 +45,6 @@ kotlin { implementation(compose.preview) implementation(libs.androidx.activity.compose) - // Koin for Android - implementation(libs.koin.android) // AndroidX WorkManager for native background tasks implementation(libs.androidx.work.runtime.ktx) // Coroutines support for Guava ListenableFuture @@ -62,9 +60,9 @@ kotlin { implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) - // Koin for dependency injection - implementation(libs.koin.core) - implementation(libs.koin.compose) + // Core KMP WorkManager (DI-agnostic) + implementation(project(":kmpworker")) + // Kotlinx Datetime for handling dates and times implementation(libs.kotlinx.datetime) // Kotlinx Serialization for JSON processing @@ -97,6 +95,28 @@ android { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } + flavorDimensions += "di" + + productFlavors { + create("manual") { + dimension = "di" + applicationIdSuffix = ".manual" + versionNameSuffix = "-manual" + } + + create("koin") { + dimension = "di" + applicationIdSuffix = ".koin" + versionNameSuffix = "-koin" + } + + create("hilt") { + dimension = "di" + applicationIdSuffix = ".hilt" + versionNameSuffix = "-hilt" + } + } + buildTypes { getByName("release") { isMinifyEnabled = false @@ -110,5 +130,17 @@ android { dependencies { debugImplementation(compose.uiTooling) + + // Manual flavor - no DI framework dependencies + // (already has core kmpworker from commonMain) + + // Koin flavor - add Koin extension + "koinImplementation"(project(":kmpworker-koin")) + "koinImplementation"(libs.koin.android) + "koinImplementation"(libs.koin.core) + "koinImplementation"(libs.koin.compose) + + // Hilt flavor - add Hilt extension (TODO: when available) + // "hiltImplementation"(project(":kmpworker-hilt")) } diff --git a/composeApp/src/androidHilt/AndroidManifest.xml b/composeApp/src/androidHilt/AndroidManifest.xml new file mode 100644 index 0000000..4edb69a --- /dev/null +++ b/composeApp/src/androidHilt/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/composeApp/src/androidHilt/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt b/composeApp/src/androidHilt/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt new file mode 100644 index 0000000..9a2b9bb --- /dev/null +++ b/composeApp/src/androidHilt/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt @@ -0,0 +1,8 @@ +package dev.brewkits.kmpworkmanager.sample + +actual object DemoConfig { + actual fun getApproachName(): String = "Hilt (Placeholder)" + + actual fun getApproachDescription(): String = + "โš ๏ธ kmpworkmanager-hilt coming soon โ€ข Currently using manual init" +} diff --git a/composeApp/src/androidHilt/kotlin/dev/brewkits/kmpworkmanager/sample/HiltApp.kt b/composeApp/src/androidHilt/kotlin/dev/brewkits/kmpworkmanager/sample/HiltApp.kt new file mode 100644 index 0000000..2fc66ac --- /dev/null +++ b/composeApp/src/androidHilt/kotlin/dev/brewkits/kmpworkmanager/sample/HiltApp.kt @@ -0,0 +1,43 @@ +package dev.brewkits.kmpworkmanager.sample + +import android.app.Application +import android.util.Log +import dev.brewkits.kmpworkmanager.WorkerManagerInitializer +import dev.brewkits.kmpworkmanager.sample.workers.SampleWorkerFactory + +/** + * Hilt Application - v2.1.0 Demo (Hilt DI Integration - Future). + * + * NOTE: kmpworkmanager-hilt extension is not yet implemented. + * This flavor currently uses manual initialization as a placeholder. + * + * Future implementation will demonstrate: + * - @HiltAndroidApp annotation + * - Hilt module for WorkerFactory + * - Native Hilt/Dagger integration + * + * Perfect for: + * - Android apps using Hilt/Dagger + * - Enterprise projects with existing Hilt infrastructure + * - Teams familiar with Dagger patterns + */ +class HiltApp : Application() { + override fun onCreate() { + super.onCreate() + + // TODO: Replace with Hilt initialization when kmpworkmanager-hilt is ready + // For now, use manual initialization + WorkerManagerInitializer.initialize( + workerFactory = SampleWorkerFactory(), + context = this + ) + + Log.i(TAG, "โœ… KMP WorkManager v2.1.0 initialized") + Log.i(TAG, "๐Ÿ“ฆ Approach: HILT (Placeholder - using manual init)") + Log.i(TAG, "โš ๏ธ kmpworkmanager-hilt extension coming soon!") + } + + companion object { + private const val TAG = "HiltApp" + } +} diff --git a/composeApp/src/androidKoin/AndroidManifest.xml b/composeApp/src/androidKoin/AndroidManifest.xml new file mode 100644 index 0000000..d1d4d4d --- /dev/null +++ b/composeApp/src/androidKoin/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/composeApp/src/androidKoin/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt b/composeApp/src/androidKoin/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt new file mode 100644 index 0000000..f1faf52 --- /dev/null +++ b/composeApp/src/androidKoin/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt @@ -0,0 +1,8 @@ +package dev.brewkits.kmpworkmanager.sample + +actual object DemoConfig { + actual fun getApproachName(): String = "Koin" + + actual fun getApproachDescription(): String = + "Koin DI โ€ข kmpworkmanager-koin extension โ€ข 100% backward compatible" +} diff --git a/composeApp/src/androidKoin/kotlin/dev/brewkits/kmpworkmanager/sample/KoinApp.kt b/composeApp/src/androidKoin/kotlin/dev/brewkits/kmpworkmanager/sample/KoinApp.kt new file mode 100644 index 0000000..68b5118 --- /dev/null +++ b/composeApp/src/androidKoin/kotlin/dev/brewkits/kmpworkmanager/sample/KoinApp.kt @@ -0,0 +1,41 @@ +package dev.brewkits.kmpworkmanager.sample + +import android.app.Application +import android.util.Log +import dev.brewkits.kmpworkmanager.koin.kmpWorkerModule +import dev.brewkits.kmpworkmanager.sample.workers.SampleWorkerFactory +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +/** + * Koin Application - v2.1.0 Demo (Koin DI Integration). + * + * This demonstrates Koin integration: + * - Uses kmpworkmanager-koin extension module + * - 100% backward compatible with v2.0.0 + * - Familiar Koin API for existing users + * + * Perfect for: + * - Apps already using Koin + * - Teams familiar with Koin patterns + * - Projects wanting DI benefits + */ +class KoinApp : Application() { + override fun onCreate() { + super.onCreate() + + // Initialize Koin with KMP WorkManager module + startKoin { + androidContext(this@KoinApp) + modules(kmpWorkerModule(SampleWorkerFactory())) + } + + Log.i(TAG, "โœ… KMP WorkManager v2.1.0 initialized") + Log.i(TAG, "๐Ÿ“ฆ Approach: KOIN Extension Module") + Log.i(TAG, "๐Ÿ’ก Benefits: Familiar API, backward compatible with v2.0.0") + } + + companion object { + private const val TAG = "KoinApp" + } +} diff --git a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/KMPWorkManagerApp.kt b/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/KMPWorkManagerApp.kt deleted file mode 100644 index e311022..0000000 --- a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/KMPWorkManagerApp.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample - -import android.app.Application -import dev.brewkits.kmpworkmanager.sample.background.data.NativeTaskScheduler -import dev.brewkits.kmpworkmanager.sample.background.domain.BackgroundTaskScheduler -import dev.brewkits.kmpworkmanager.sample.debug.AndroidDebugSource -import dev.brewkits.kmpworkmanager.sample.debug.DebugSource -import dev.brewkits.kmpworkmanager.sample.di.initKoin -import org.koin.android.ext.koin.androidContext -import org.koin.android.ext.koin.androidLogger -import org.koin.dsl.module - -/** - * The main Application class for Android. - * Responsible for initializing Koin and providing the Android-specific implementation - * of the BackgroundTaskScheduler. - */ -class KMPWorkManagerApp : Application() { - override fun onCreate() { - super.onCreate() - - val androidModule = module { - single { NativeTaskScheduler(androidContext()) } - single { AndroidDebugSource(androidContext()) } - } - - initKoin { - androidLogger() - androidContext(this@KMPWorkManagerApp) - modules(androidModule) - } - } -} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/MainActivity.kt b/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/MainActivity.kt index cded75f..d4863f2 100644 --- a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/MainActivity.kt @@ -4,22 +4,33 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +/** + * Main Activity for KMP WorkManager v2.1.0 Demo. + * + * This activity is shared across all build variants (manual, koin, hilt). + * The DI approach is determined by the Application class (flavor-specific). + */ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { - App() + MaterialTheme { + DemoScreen() + } } } } @Preview @Composable -fun AppAndroidPreview() { - App() +fun DemoScreenPreview() { + MaterialTheme { + DemoScreen() + } } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/Notification.kt b/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/Notification.kt deleted file mode 100644 index 1504285..0000000 --- a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/Notification.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import androidx.core.app.NotificationCompat -import org.koin.mp.KoinPlatform.getKoin - -actual fun showNotification(title: String, body: String) { - val context: Context = getKoin().get() - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val channelId = "test_notification_channel" - val channel = NotificationChannel(channelId, "Test Notifications", NotificationManager.IMPORTANCE_DEFAULT) - notificationManager.createNotificationChannel(channel) - - val notification = NotificationCompat.Builder(context, channelId) - .setSmallIcon(R.drawable.ic_launcher_foreground) - .setContentTitle(title) - .setContentText(body) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .build() - - notificationManager.notify(System.currentTimeMillis().toInt(), notification) -} diff --git a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/AndroidDebugSource.kt b/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/AndroidDebugSource.kt deleted file mode 100644 index e56ad5d..0000000 --- a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/AndroidDebugSource.kt +++ /dev/null @@ -1,38 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.debug - -import android.content.Context -import androidx.work.WorkInfo -import androidx.work.WorkManager -import dev.brewkits.kmpworkmanager.sample.background.data.NativeTaskScheduler -import com.google.common.util.concurrent.ListenableFuture -import kotlinx.coroutines.guava.await - -/** - * Android-specific implementation of DebugSource that queries WorkManager to get task information. - */ -class AndroidDebugSource(private val context: Context) : DebugSource { - - private val workManager = WorkManager.getInstance(context) - - override suspend fun getTasks(): List { - // Query WorkManager for all tasks with our common tag - val future: ListenableFuture> = workManager.getWorkInfosByTag(NativeTaskScheduler.TAG_KMP_TASK) - val workInfos = future.await() // Use await from kotlinx-coroutines-guava - - return workInfos.map { workInfo -> - // Extract metadata from tags and data - val id = workInfo.tags.firstOrNull { it.startsWith("id-") }?.substringAfter("id-") ?: workInfo.id.toString() - val type = workInfo.tags.firstOrNull { it.startsWith("type-") }?.substringAfter("type-") ?: "Unknown" - val workerClassName = workInfo.tags.firstOrNull { it.startsWith("worker-") }?.substringAfter("worker-") ?: "N/A" - - DebugTaskInfo( - id = id, - type = type.replaceFirstChar { it.uppercase() }, - status = workInfo.state.name, - workerClassName = workerClassName.split('.').last(), // Show simple name - isPeriodic = workInfo.tags.contains("type-periodic"), - isChain = workInfo.tags.contains("type-chain-member") - ) - } - } -} diff --git a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/PushReceiver.kt b/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/PushReceiver.kt deleted file mode 100644 index 40030be..0000000 --- a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/PushReceiver.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.push - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import dev.brewkits.kmpworkmanager.sample.background.data.WorkerTypes -import dev.brewkits.kmpworkmanager.sample.background.domain.BackgroundTaskScheduler -import dev.brewkits.kmpworkmanager.sample.background.domain.TaskTrigger -import org.koin.core.context.GlobalContext - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class PushReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val koin = GlobalContext.get() - val scheduler: BackgroundTaskScheduler = koin.get() - - val pendingResult = goAsync() - CoroutineScope(Dispatchers.IO).launch { - try { - scheduler.enqueue( - id = "task-from-push", - trigger = TaskTrigger.OneTime(initialDelayMs = 5000), - workerClassName = WorkerTypes.UPLOAD_WORKER - ) - } finally { - pendingResult.finish() - } - } - } -} diff --git a/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/SampleWorkerFactory.kt b/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/SampleWorkerFactory.kt new file mode 100644 index 0000000..e78d939 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/SampleWorkerFactory.kt @@ -0,0 +1,47 @@ +package dev.brewkits.kmpworkmanager.sample.workers + +import dev.brewkits.kmpworkmanager.background.domain.AndroidWorker +import dev.brewkits.kmpworkmanager.background.domain.AndroidWorkerFactory + +/** + * Sample WorkerFactory for Android - v2.1.0 Demo App. + * + * This factory demonstrates how to: + * - Implement AndroidWorkerFactory for v2.1.0 + * - Register workers by class name + * - Use common worker implementations + */ +class SampleWorkerFactory : AndroidWorkerFactory { + override fun createWorker(workerClassName: String): AndroidWorker? { + return when (workerClassName) { + "DemoWorker" -> DemoWorkerAndroid() + "HeavyWorker" -> HeavyWorkerAndroid() + else -> { + println("โŒ Unknown worker: $workerClassName") + null + } + } + } +} + +/** + * Android wrapper for DemoWorker. + */ +private class DemoWorkerAndroid : AndroidWorker { + private val worker = DemoWorker() + + override suspend fun doWork(input: String?): Boolean { + return worker.doWork(input) + } +} + +/** + * Android wrapper for HeavyWorker. + */ +private class HeavyWorkerAndroid : AndroidWorker { + private val worker = HeavyWorker() + + override suspend fun doWork(input: String?): Boolean { + return worker.doWork(input) + } +} diff --git a/composeApp/src/androidManual/AndroidManifest.xml b/composeApp/src/androidManual/AndroidManifest.xml new file mode 100644 index 0000000..054ac37 --- /dev/null +++ b/composeApp/src/androidManual/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/composeApp/src/androidManual/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt b/composeApp/src/androidManual/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt new file mode 100644 index 0000000..bb2539b --- /dev/null +++ b/composeApp/src/androidManual/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt @@ -0,0 +1,8 @@ +package dev.brewkits.kmpworkmanager.sample + +actual object DemoConfig { + actual fun getApproachName(): String = "Manual" + + actual fun getApproachDescription(): String = + "No DI framework โ€ข Zero dependencies โ€ข Direct WorkerManagerInitializer.initialize()" +} diff --git a/composeApp/src/androidManual/kotlin/dev/brewkits/kmpworkmanager/sample/ManualApp.kt b/composeApp/src/androidManual/kotlin/dev/brewkits/kmpworkmanager/sample/ManualApp.kt new file mode 100644 index 0000000..be2a4e9 --- /dev/null +++ b/composeApp/src/androidManual/kotlin/dev/brewkits/kmpworkmanager/sample/ManualApp.kt @@ -0,0 +1,39 @@ +package dev.brewkits.kmpworkmanager.sample + +import android.app.Application +import android.util.Log +import dev.brewkits.kmpworkmanager.WorkerManagerInitializer +import dev.brewkits.kmpworkmanager.sample.workers.SampleWorkerFactory + +/** + * Manual Application - v2.1.0 Demo (No DI Framework). + * + * This demonstrates the simplest initialization approach: + * - No dependency injection framework required + * - Direct WorkerManagerInitializer.initialize() call + * - Zero external dependencies (besides the core library) + * + * Perfect for: + * - New projects wanting minimal dependencies + * - Lightweight apps + * - Testing without DI complexity + */ +class ManualApp : Application() { + override fun onCreate() { + super.onCreate() + + // Initialize KMP WorkManager manually - no DI needed! + WorkerManagerInitializer.initialize( + workerFactory = SampleWorkerFactory(), + context = this + ) + + Log.i(TAG, "โœ… KMP WorkManager v2.1.0 initialized") + Log.i(TAG, "๐Ÿ“ฆ Approach: MANUAL (No DI Framework)") + Log.i(TAG, "๐Ÿ’ก Benefits: Zero DI dependencies, simple setup") + } + + companion object { + private const val TAG = "ManualApp" + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/App.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/App.kt deleted file mode 100644 index c3563bd..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/App.kt +++ /dev/null @@ -1,877 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import dev.brewkits.kmpworkmanager.sample.background.data.TaskIds -import dev.brewkits.kmpworkmanager.sample.background.data.WorkerTypes -import dev.brewkits.kmpworkmanager.sample.background.domain.BackgroundTaskScheduler -import dev.brewkits.kmpworkmanager.sample.background.domain.Constraints -import dev.brewkits.kmpworkmanager.sample.background.domain.TaskRequest -import dev.brewkits.kmpworkmanager.sample.background.domain.TaskTrigger -import dev.brewkits.kmpworkmanager.sample.background.domain.TaskEventBus -import dev.brewkits.kmpworkmanager.sample.background.domain.TaskCompletionEvent -import dev.brewkits.kmpworkmanager.sample.background.domain.ScheduleResult -import dev.brewkits.kmpworkmanager.sample.debug.DebugScreen -import dev.brewkits.kmpworkmanager.sample.push.FakePushNotificationHandler -import dev.brewkits.kmpworkmanager.sample.push.PushNotificationHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.jetbrains.compose.ui.tooling.preview.Preview -import org.koin.mp.KoinPlatform.getKoin -import kotlin.time.Clock -import kotlin.time.Duration.Companion.minutes -import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime - -/** - * The main entry point of the application. - * This composable function is responsible for rendering the entire UI, handling dependencies - * (via parameters or Koin), and managing core states like permissions. - * - * @param scheduler The platform-specific background task scheduler instance. - * @param pushHandler The platform-specific push notification handler instance. - */ -@OptIn(ExperimentalTime::class, ExperimentalFoundationApi::class) -@Composable -fun App( - // Dependencies are injected, defaulting to Koin lookup if not provided (for production code) - scheduler: BackgroundTaskScheduler = getKoin().get(), - pushHandler: PushNotificationHandler = getKoin().get() -) { - // State for holding the status text to be displayed on the UI. - var statusText by remember { mutableStateOf("Requesting permissions...") } - - // Coroutine scope for launching asynchronous operations from UI events (e.g., button clicks). - val coroutineScope = rememberCoroutineScope() - - // State for managing notification permission using the platform-specific implementation. - val notificationPermissionState = rememberNotificationPermissionState { isGranted -> - statusText = - if (isGranted) "Notification permission granted." else "Notification permission denied." - } - - // State for managing exact alarm permission on Android (iOS implementation is a no-op/always true). - val exactAlarmPermissionState = rememberExactAlarmPermissionState() - - // State for managing the horizontal pager (tab view). - val pagerState = rememberPagerState(pageCount = { 6 }) - - // Snackbar host state for showing toast messages - val snackbarHostState = remember { SnackbarHostState() } - - // Listen for task completion events and show snackbar - LaunchedEffect(Unit) { - TaskEventBus.events.collect { event -> - snackbarHostState.showSnackbar( - message = event.message, - duration = SnackbarDuration.Long - ) - } - } - - MaterialTheme { - Scaffold( - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) { data -> - Snackbar( - action = { - TextButton(onClick = { data.dismiss() }) { - Text("Close") - } - }, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) { - Text(data.visuals.message) - } - } - } - ) { paddingValues -> - Column( - Modifier.fillMaxSize() - // Apply padding for system bars (e.g., status bar, navigation bar) - .padding(WindowInsets.systemBars.asPaddingValues()) - ) { - // Tab bar for navigation - PrimaryTabRow(selectedTabIndex = pagerState.currentPage) { - Tab(selected = pagerState.currentPage == 0, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } }) { Text("Test & Demo", modifier = Modifier.padding(16.dp)) } - Tab(selected = pagerState.currentPage == 1, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }) { Text("Tasks", modifier = Modifier.padding(16.dp)) } - Tab(selected = pagerState.currentPage == 2, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } }) { Text("Chains", modifier = Modifier.padding(16.dp)) } - Tab(selected = pagerState.currentPage == 3, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(3) } }) { Text("Alarms", modifier = Modifier.padding(16.dp)) } - Tab(selected = pagerState.currentPage == 4, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(4) } }) { Text("Permissions", modifier = Modifier.padding(16.dp)) } - Tab(selected = pagerState.currentPage == 5, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(5) } }) { Text("Debug", modifier = Modifier.padding(16.dp)) } - } - - // Horizontal pager to host the different tab screens - HorizontalPager(state = pagerState) { - when (it) { - 0 -> TestDemoTab(scheduler, coroutineScope, snackbarHostState) - 1 -> TasksTab(scheduler, coroutineScope, statusText, snackbarHostState) - 2 -> TaskChainsTab(scheduler, coroutineScope, snackbarHostState) - 3 -> AlarmsAndPushTab(scheduler, coroutineScope, statusText, exactAlarmPermissionState, snackbarHostState) - 4 -> PermissionsAndInfoTab(notificationPermissionState, exactAlarmPermissionState) - 5 -> DebugScreen() - } - } - } - } - } -} - -/** - * Test & Demo tab - Easy-to-test features that work in foreground - */ -@OptIn(ExperimentalTime::class) -@Composable -fun TestDemoTab(scheduler: BackgroundTaskScheduler, coroutineScope: CoroutineScope, snackbarHostState: SnackbarHostState) { - Column( - Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text("Quick Test & Demo", style = MaterialTheme.typography.headlineSmall) - Text("All features here work instantly in foreground!", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary) - Spacer(modifier = Modifier.height(16.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("1. EventBus & Toast System", style = MaterialTheme.typography.titleLarge) - InfoBox("Test the event bus system that workers use to communicate with UI.") - Spacer(modifier = Modifier.height(12.dp)) - - Button( - onClick = { - coroutineScope.launch { - TaskEventBus.emit( - TaskCompletionEvent( - taskName = "EventBus Test", - success = true, - message = "โœ… EventBus is working! Toast displayed successfully." - ) - ) - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Test EventBus โ†’ Toast") - } - Text("โœ“ Instantly shows toast message", style = MaterialTheme.typography.bodySmall) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("2. Simulated Worker Execution", style = MaterialTheme.typography.titleLarge) - InfoBox("Simulate a worker running and completing (like what happens in background).") - Spacer(modifier = Modifier.height(12.dp)) - - Button( - onClick = { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "โš™๏ธ Worker started...", - duration = SnackbarDuration.Short - ) - - kotlinx.coroutines.delay(2000) - - TaskEventBus.emit( - TaskCompletionEvent( - taskName = "Upload Worker", - success = true, - message = "๐Ÿ“ค Simulated: Uploaded 100MB successfully!" - ) - ) - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Simulate Upload Worker (2s)") - } - Text("โœ“ Shows progress โ†’ completion toast", style = MaterialTheme.typography.bodySmall) - - Spacer(modifier = Modifier.height(8.dp)) - - Button( - onClick = { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "๐Ÿ”„ Syncing data...", - duration = SnackbarDuration.Short - ) - - kotlinx.coroutines.delay(1500) - - TaskEventBus.emit( - TaskCompletionEvent( - taskName = "Sync Worker", - success = true, - message = "๐Ÿ”„ Simulated: Data synced successfully!" - ) - ) - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Simulate Sync Worker (1.5s)") - } - Text("โœ“ Shows sync โ†’ success toast", style = MaterialTheme.typography.bodySmall) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("3. Task Scheduling (Both Platforms)", style = MaterialTheme.typography.titleLarge) - InfoBox("Schedule tasks on native schedulers. Check Debug tab to see scheduled tasks.") - Spacer(modifier = Modifier.height(12.dp)) - - Button( - onClick = { - coroutineScope.launch { - val timestamp = Clock.System.now().toEpochMilliseconds() - scheduler.enqueue( - id = "demo-task-$timestamp", - trigger = TaskTrigger.OneTime(initialDelayMs = 5000), - workerClassName = WorkerTypes.SYNC_WORKER - ) - snackbarHostState.showSnackbar( - message = "โœ… Task scheduled! Check Debug tab to verify.", - duration = SnackbarDuration.Short - ) - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Schedule Task (Check Debug Tab)") - } - Text("โœ“ Android: WorkManager | iOS: BGTaskScheduler", style = MaterialTheme.typography.bodySmall) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("4. Task Chain Simulation", style = MaterialTheme.typography.titleLarge) - InfoBox("Simulate a chain of workers executing sequentially.") - Spacer(modifier = Modifier.height(12.dp)) - - Button( - onClick = { - coroutineScope.launch { - // Step 1 - snackbarHostState.showSnackbar( - message = "๐Ÿ”— Step 1/3: Syncing...", - duration = SnackbarDuration.Short - ) - kotlinx.coroutines.delay(1000) - - // Step 2 - snackbarHostState.showSnackbar( - message = "๐Ÿ”— Step 2/3: Uploading...", - duration = SnackbarDuration.Short - ) - kotlinx.coroutines.delay(1500) - - // Step 3 - snackbarHostState.showSnackbar( - message = "๐Ÿ”— Step 3/3: Final sync...", - duration = SnackbarDuration.Short - ) - kotlinx.coroutines.delay(1000) - - // Complete - TaskEventBus.emit( - TaskCompletionEvent( - taskName = "Task Chain", - success = true, - message = "โœ… Simulated: Chain completed! (Sync โ†’ Upload โ†’ Sync)" - ) - ) - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Simulate Task Chain (3.5s)") - } - Text("โœ“ Shows all 3 steps + completion", style = MaterialTheme.typography.bodySmall) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text("5. Failure Scenarios", style = MaterialTheme.typography.titleLarge) - InfoBox("Test how the app handles failures and errors.") - Spacer(modifier = Modifier.height(12.dp)) - - Button( - onClick = { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "โš ๏ธ Worker started...", - duration = SnackbarDuration.Short - ) - - kotlinx.coroutines.delay(1500) - - TaskEventBus.emit( - TaskCompletionEvent( - taskName = "Upload Worker", - success = false, - message = "โŒ Simulated: Upload failed! Network error." - ) - ) - } - }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) - ) { - Text("Simulate Failed Worker") - } - Text("โœ“ Shows failure toast", style = MaterialTheme.typography.bodySmall) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Surface( - modifier = Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.secondaryContainer - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text("๐Ÿ’ก Testing Background Tasks (iOS)", style = MaterialTheme.typography.titleMedium) - Text( - "iOS BGTaskScheduler tasks only run in background:\n\n" + - "1. Schedule task in 'Tasks' tab\n" + - "2. Press Home button (app to background)\n" + - "3. Wait for iOS to execute\n" + - "4. Open app โ†’ See completion toast\n\n" + - "Or use Xcode LLDB:\n" + - "e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@\"one-time-upload\"]", - style = MaterialTheme.typography.bodySmall - ) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Surface( - modifier = Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.tertiaryContainer - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text("๐Ÿ’ก Testing Background Tasks (Android)", style = MaterialTheme.typography.titleMedium) - Text( - "Android WorkManager runs even in foreground:\n\n" + - "1. Schedule task in 'Tasks' tab\n" + - "2. Wait for delay time\n" + - "3. Toast appears automatically\n\n" + - "Tasks run reliably with WorkManager!", - style = MaterialTheme.typography.bodySmall - ) - } - } - } -} - -/** - * Composable for scheduling and managing background tasks (WorkManager/BGTaskScheduler). - */ -@Composable -fun TasksTab(scheduler: BackgroundTaskScheduler, coroutineScope: CoroutineScope, statusText: String, snackbarHostState: SnackbarHostState) { - Column( - Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text("WorkManager / BGTaskScheduler", style = MaterialTheme.typography.headlineSmall) - Spacer(modifier = Modifier.height(16.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Run a task once", style = MaterialTheme.typography.titleLarge) - InfoBox("Schedule a task to run once in the future.") - Spacer(modifier = Modifier.height(16.dp)) - // Button to schedule a regular one-time task - Button(onClick = { - coroutineScope.launch { - val result = scheduler.enqueue( - id = TaskIds.ONE_TIME_UPLOAD, - trigger = TaskTrigger.OneTime(initialDelayMs = 10.seconds.inWholeMilliseconds), - workerClassName = WorkerTypes.UPLOAD_WORKER - ) - snackbarHostState.showSnackbar( - message = "โœ… Background task scheduled! Will run in 10s", - duration = SnackbarDuration.Short - ) - } - }) { - Text("Run BG Task in 10s") - } - Text("โš™๏ธ Run a one-time background task after 10 seconds.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(16.dp)) - - // Button to schedule a heavy/processing task - Button(onClick = { - coroutineScope.launch { - val result = scheduler.enqueue( - id = TaskIds.HEAVY_TASK_1, - trigger = TaskTrigger.OneTime(initialDelayMs = 5.seconds.inWholeMilliseconds), - workerClassName = WorkerTypes.HEAVY_PROCESSING_WORKER, - constraints = Constraints(isHeavyTask = true) // Mark as a heavy task for platform - ) - val message = when (result) { - ScheduleResult.ACCEPTED -> "โšก Heavy task scheduled! Will run in 5s" - ScheduleResult.REJECTED_OS_POLICY -> "โŒ Task rejected by OS policy" - ScheduleResult.THROTTLED -> "โณ Task throttled, will retry later" - } - snackbarHostState.showSnackbar( - message = message, - duration = SnackbarDuration.Short - ) - } - }) { - Text("Schedule Heavy Task (30s)") - } - Text("โšก Run a heavy background task (Foreground Service / BGProcessingTask).", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - InfoBox("Note: Heavy tasks on iOS have a time limit (usually around 30 minutes) and require the device to be charging and connected to a network.") - Spacer(modifier = Modifier.height(16.dp)) - - // Button to schedule a task with network constraints - Button(onClick = { - coroutineScope.launch { - val result = scheduler.enqueue( - id = "network-task", - trigger = TaskTrigger.OneTime(initialDelayMs = 5.seconds.inWholeMilliseconds), - workerClassName = WorkerTypes.UPLOAD_WORKER, - constraints = Constraints(requiresNetwork = true) - ) - snackbarHostState.showSnackbar( - message = "๐ŸŒ Network task scheduled! Will run when connected", - duration = SnackbarDuration.Short - ) - } - }) { - Text("Schedule Task with Network Constraint") - } - Text("๐ŸŒ Schedule a task that requires a network connection.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - InfoBox("Note: On Android, this uses WorkManager's network constraints. The task will only run when the device has a network connection.\n\nNote: On iOS, network constraints are only supported for heavy tasks (BGProcessingTask). Regular tasks (BGAppRefreshTask) do not support this constraint.") - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Run a task repeatedly", style = MaterialTheme.typography.titleLarge) - InfoBox("Schedule a task to run periodically in the background.") - Spacer(modifier = Modifier.height(16.dp)) - // Button to schedule a periodic task - Button(onClick = { - coroutineScope.launch { - val result = scheduler.enqueue( - id = TaskIds.PERIODIC_SYNC_TASK, - trigger = TaskTrigger.Periodic(intervalMs = 15.minutes.inWholeMilliseconds), - workerClassName = WorkerTypes.SYNC_WORKER - ) - snackbarHostState.showSnackbar( - message = "๐Ÿ”„ Periodic sync scheduled! Will run every 15 min", - duration = SnackbarDuration.Short - ) - } - }) { - Text("Schedule Periodic Sync (15 min)") - } - Text("๐Ÿ”„ Schedule a recurring task using BGTaskScheduler/WorkManager.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - InfoBox("Note: Periodic tasks on Android are not exact and may be delayed by Doze mode. The minimum interval is 15 minutes.\n\nNote: Periodic tasks on iOS are not guaranteed to run. The system decides when to run them based on app usage, battery, and network conditions. The minimum interval is not guaranteed.") - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Advanced Triggers (Android Only)", style = MaterialTheme.typography.titleLarge) - InfoBox("These triggers use Android-specific features and will be rejected on iOS.") - Spacer(modifier = Modifier.height(16.dp)) - - // ContentUri trigger - Button(onClick = { - coroutineScope.launch { - val result = scheduler.enqueue( - id = "content-uri-task", - trigger = TaskTrigger.ContentUri( - uriString = "content://media/external/images/media", - triggerForDescendants = true - ), - workerClassName = WorkerTypes.SYNC_WORKER - ) - val message = when (result) { - ScheduleResult.ACCEPTED -> "๐Ÿ“ธ ContentUri trigger scheduled! Will run when images change" - ScheduleResult.REJECTED_OS_POLICY -> "โŒ ContentUri not supported on this platform (Android only)" - ScheduleResult.THROTTLED -> "โณ Task throttled, will retry later" - } - snackbarHostState.showSnackbar( - message = message, - duration = SnackbarDuration.Short - ) - } - }) { - Text("Monitor Image Content Changes") - } - Text("๐Ÿ“ธ Triggers when MediaStore images change.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(8.dp)) - - // BatteryOkay trigger - Button(onClick = { - coroutineScope.launch { - val result = scheduler.enqueue( - id = "battery-okay-task", - trigger = TaskTrigger.BatteryOkay, - workerClassName = WorkerTypes.SYNC_WORKER - ) - snackbarHostState.showSnackbar( - message = "๐Ÿ”‹ BatteryOkay trigger scheduled! Will run when battery is not low", - duration = SnackbarDuration.Short - ) - } - }) { - Text("Run When Battery Is Okay") - } - Text("๐Ÿ”‹ Only runs when battery is not low.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(8.dp)) - - // DeviceIdle trigger - Button(onClick = { - coroutineScope.launch { - val result = scheduler.enqueue( - id = "device-idle-task", - trigger = TaskTrigger.DeviceIdle, - workerClassName = WorkerTypes.HEAVY_PROCESSING_WORKER, - constraints = Constraints(isHeavyTask = true) - ) - snackbarHostState.showSnackbar( - message = "๐Ÿ’ค DeviceIdle trigger scheduled! Will run when device is idle", - duration = SnackbarDuration.Short - ) - } - }) { - Text("Run When Device Is Idle") - } - Text("๐Ÿ’ค Only runs when device is idle (screen off, not moving).", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(8.dp)) - - InfoBox("Note: These triggers are Android-only and use WorkManager's advanced constraints. ContentUri triggers fire when content changes, while BatteryOkay and DeviceIdle are constraint-based triggers.\n\nOn iOS, these triggers will be rejected with REJECTED_OS_POLICY status.") - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Task Management", style = MaterialTheme.typography.titleLarge) - InfoBox("Cancel scheduled tasks or clear all pending work.") - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - // Cancel specific task - Button( - onClick = { - coroutineScope.launch { - scheduler.cancel(TaskIds.ONE_TIME_UPLOAD) - snackbarHostState.showSnackbar( - message = "๐Ÿšซ Cancelled ONE_TIME_UPLOAD task", - duration = SnackbarDuration.Short - ) - } - }, - modifier = Modifier.weight(1f) - ) { - Text("Cancel Upload Task", style = MaterialTheme.typography.bodySmall) - } - - // Cancel periodic task - Button( - onClick = { - coroutineScope.launch { - scheduler.cancel(TaskIds.PERIODIC_SYNC_TASK) - snackbarHostState.showSnackbar( - message = "๐Ÿšซ Cancelled PERIODIC_SYNC task", - duration = SnackbarDuration.Short - ) - } - }, - modifier = Modifier.weight(1f) - ) { - Text("Cancel Periodic", style = MaterialTheme.typography.bodySmall) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - // Cancel all tasks - Button( - onClick = { - coroutineScope.launch { - scheduler.cancelAll() - snackbarHostState.showSnackbar( - message = "๐Ÿšซ All tasks cancelled!", - duration = SnackbarDuration.Short - ) - } - }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) - ) { - Text("Cancel All Tasks") - } - - Spacer(modifier = Modifier.height(8.dp)) - Text("โš ๏ธ Cancel specific tasks by ID or clear all pending work.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - } - } - } -} - -/** - * Composable for demonstrating task chaining functionality. - */ -@Composable -fun TaskChainsTab(scheduler: BackgroundTaskScheduler, coroutineScope: CoroutineScope, snackbarHostState: SnackbarHostState) { - Column( - Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text("Task Chains", style = MaterialTheme.typography.headlineSmall) - Spacer(modifier = Modifier.height(16.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Sequential & Parallel Task Execution", style = MaterialTheme.typography.titleLarge) - InfoBox("Task chains allow you to execute multiple tasks in sequence or in parallel. This is useful for complex workflows that require multiple steps.") - Spacer(modifier = Modifier.height(16.dp)) - - // Example 1: Simple Sequential Chain - Button(onClick = { - coroutineScope.launch { - scheduler.beginWith(TaskRequest(workerClassName = WorkerTypes.SYNC_WORKER)) - .then(TaskRequest(workerClassName = WorkerTypes.UPLOAD_WORKER)) - .then(TaskRequest(workerClassName = WorkerTypes.SYNC_WORKER, inputJson = "{\"status\":\"complete\"}")) - .enqueue() - snackbarHostState.showSnackbar( - message = "๐Ÿ”— Sequential chain started!", - duration = SnackbarDuration.Short - ) - } - }) { - Text("Run Sequential Chain") - } - Text("๐Ÿ”— Execute: Sync โ†’ Upload โ†’ Sync", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(16.dp)) - - // Example 2: Sequential + Parallel Chain - Button(onClick = { - coroutineScope.launch { - scheduler.beginWith(TaskRequest(workerClassName = WorkerTypes.SYNC_WORKER)) - .then( - listOf( - TaskRequest(workerClassName = WorkerTypes.UPLOAD_WORKER), - TaskRequest( - workerClassName = WorkerTypes.HEAVY_PROCESSING_WORKER, - constraints = Constraints(isHeavyTask = true) - ) - ) - ) - .then(TaskRequest(workerClassName = WorkerTypes.SYNC_WORKER, inputJson = "{\"status\":\"complete\"}")) - .enqueue() - snackbarHostState.showSnackbar( - message = "๐Ÿ”€ Mixed chain started! Running parallel tasks...", - duration = SnackbarDuration.Short - ) - } - }) { - Text("Run Mixed Chain") - } - Text("๐Ÿ”€ Execute: Sync โ†’ (Upload โˆฅ Heavy Processing) โ†’ Sync", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - InfoBox("This chain starts with a sync, then runs upload and heavy processing in parallel, and finishes with another sync.") - Spacer(modifier = Modifier.height(16.dp)) - - // Example 3: Parallel Start Chain - Button(onClick = { - coroutineScope.launch { - scheduler.beginWith( - listOf( - TaskRequest(workerClassName = WorkerTypes.SYNC_WORKER), - TaskRequest(workerClassName = WorkerTypes.UPLOAD_WORKER) - ) - ) - .then(TaskRequest(workerClassName = WorkerTypes.SYNC_WORKER, inputJson = "{\"status\":\"done\"}")) - .enqueue() - snackbarHostState.showSnackbar( - message = "โšก Parallel start chain launched!", - duration = SnackbarDuration.Short - ) - } - }) { - Text("Run Parallel Start Chain") - } - Text("โšก Execute: (Sync โˆฅ Upload) โ†’ Sync", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(16.dp)) - - InfoBox("Note on Android: Task chains use WorkManager's continuation API. Tasks in parallel groups run concurrently.\n\nNote on iOS: Task chains are serialized and stored in UserDefaults. A special chain executor task processes them step by step. Parallel tasks within a step are executed using coroutines.") - } - } - } -} - -/** - * Composable for scheduling exact alarms/reminders (AlarmManager/UserNotifications) and Push Notifications info. - */ -@OptIn(ExperimentalTime::class) -@Composable -fun AlarmsAndPushTab(scheduler: BackgroundTaskScheduler, coroutineScope: CoroutineScope, statusText: String, exactAlarmPermissionState: ExactAlarmPermissionState, snackbarHostState: SnackbarHostState) { - Column( - Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("AlarmManager / UserNotifications", style = MaterialTheme.typography.headlineSmall) - Spacer(modifier = Modifier.height(8.dp)) - // Button to schedule an exact alarm (enabled only if permission is granted) - Button(onClick = { - coroutineScope.launch { - // Calculate time 10 seconds from now - val reminderTime = Clock.System.now().plus(10.seconds).toEpochMilliseconds() - val result = scheduler.enqueue( - id = TaskIds.EXACT_REMINDER, - trigger = TaskTrigger.Exact(atEpochMillis = reminderTime), - workerClassName = "Reminder" - ) - snackbarHostState.showSnackbar( - message = "โฐ Reminder set! Will notify in 10s", - duration = SnackbarDuration.Short - ) - } - }, enabled = exactAlarmPermissionState.hasPermission) { - Text("Schedule Reminder in 10s") - } - Text("โฐ Set an exact alarm/notification.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center) - InfoBox("Note: On Android 14+, the USE_EXACT_ALARM permission is required. If not granted, the reminder will not be exact.\n\nNote: Exact alarms on iOS are scheduled as local notifications. The app is not guaranteed to be woken up to run code at the exact time.") - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Push Notifications", style = MaterialTheme.typography.headlineSmall) - Spacer(modifier = Modifier.height(8.dp)) - Text( - "When a silent push is received, the app will schedule a background task to run after 5 seconds. " + - "When the task is completed, it will show a local notification.", - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(8.dp)) - InfoBox("To test on Android, send a push notification to the app while it's in the background. You can use the Firebase console or a tool like `adb`.\n\nTo test on iOS, send a silent push notification to the simulator using the `xcrun simctl push` command. Make sure the `push.apns` file contains `\"content-available\": 1`.") - } - } - } -} - -/** - * Composable for displaying current permission states and providing grant buttons. - */ -@Composable -fun PermissionsAndInfoTab(notificationPermissionState: NotificationPermissionState, exactAlarmPermissionState: ExactAlarmPermissionState) { - Column( - Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Permissions", style = MaterialTheme.typography.headlineSmall) - Spacer(modifier = Modifier.height(8.dp)) - // Notification Permission UI - if (notificationPermissionState.shouldShowRequest) { - Button(onClick = notificationPermissionState.requestPermission) { - Text("Grant Notification Permission") - } - Text("Notification permission is required to show notifications.", style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(8.dp)) - } else { - Text("Notification permission has been granted.", style = MaterialTheme.typography.bodyMedium) - Spacer(modifier = Modifier.height(8.dp)) - } - // Exact Alarm Permission UI - if (exactAlarmPermissionState.shouldShowRequest) { - Button(onClick = exactAlarmPermissionState.requestPermission) { - Text("Grant Exact Alarm Permission") - } - Text("Exact reminders require a special permission on Android 12+.", style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center) - } else { - Text("Exact alarm permission has been granted.", style = MaterialTheme.typography.bodyMedium) - } - } - } - } -} - -/** - * A utility composable to display informational notes with a distinct styling. - */ -@OptIn(ExperimentalTime::class) -@Composable -fun InfoBox(text: String) { - Surface( - modifier = Modifier.fillMaxWidth().padding(8.dp), - shape = MaterialTheme.shapes.medium, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) { - Text( - text = text, - modifier = Modifier.padding(16.dp), - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary - ) - } -} - -/** - * Preview composable for the App, using fake schedulers for safe rendering. - */ -@Preview -@Composable -fun AppPreview() { - App(scheduler = FakeBackgroundTaskScheduler(), pushHandler = FakePushNotificationHandler()) -} diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt new file mode 100644 index 0000000..ad0a755 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/DemoConfig.kt @@ -0,0 +1,21 @@ +package dev.brewkits.kmpworkmanager.sample + +/** + * Demo configuration for displaying current DI approach. + * + * This is populated by platform-specific implementations to show + * which build variant is currently running. + */ +expect object DemoConfig { + /** + * Returns the current DI approach name. + * + * Examples: "Manual", "Koin", "Hilt" + */ + fun getApproachName(): String + + /** + * Returns a description of the current approach. + */ + fun getApproachDescription(): String +} diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/DemoScreen.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/DemoScreen.kt new file mode 100644 index 0000000..83a7ce4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/DemoScreen.kt @@ -0,0 +1,138 @@ +package dev.brewkits.kmpworkmanager.sample + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +/** + * Demo screen for v2.1.0 - shows current DI approach and provides + * simple task scheduling buttons. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DemoScreen() { + var statusText by remember { mutableStateOf("Ready") } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("KMP WorkManager v2.1.0 Demo") }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Current approach card + ApproachCard() + + Divider() + + // Status text + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Text( + text = "Status: $statusText", + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.bodyLarge + ) + } + + // Action buttons + Text( + "Demo Actions", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Button( + onClick = { + statusText = "Scheduling demo task..." + // TODO: Schedule task via WorkerManagerInitializer.getScheduler() + statusText = "Task scheduled!" + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Schedule Demo Task") + } + + Button( + onClick = { + statusText = "Scheduling heavy task..." + // TODO: Schedule heavy task + statusText = "Heavy task scheduled!" + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Schedule Heavy Task") + } + + OutlinedButton( + onClick = { + statusText = "Demo initialized successfully" + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Test Status Update") + } + } + } +} + +@Composable +private fun ApproachCard() { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + "Current Approach", + style = MaterialTheme.typography.labelMedium + ) + + Text( + DemoConfig.getApproachName(), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + + Text( + DemoConfig.getApproachDescription(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + "๐Ÿ’ก Switch build variants in Android Studio to test different approaches", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f) + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/Notification.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/Notification.kt deleted file mode 100644 index 29bf66a..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/Notification.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample - -/** - * Expected function declaration to display a simple local notification. - * The actual implementation will use platform-specific APIs (e.g., NotificationManager on Android, UNUserNotificationCenter on iOS). - * - * @param title The title of the notification. - * @param body The body/content of the notification. - */ -expect fun showNotification(title: String, body: String) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugScreen.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugScreen.kt deleted file mode 100644 index 9f43c51..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugScreen.kt +++ /dev/null @@ -1,97 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.debug - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DebugScreen() { - val viewModel = remember { DebugViewModel() } - val tasks by viewModel.tasks.collectAsState() - - LaunchedEffect(Unit) { - viewModel.refresh() - } - - Scaffold( - topBar = { - TopAppBar( - title = { Text("Background Task Debugger") }, - actions = { - Button(onClick = { viewModel.refresh() }) { Text("Refresh") } - } - ) - } - ) { - paddingValues -> - LazyColumn(modifier = Modifier.fillMaxSize().padding(paddingValues)) { - if (tasks.isEmpty()) { - item { - Box(modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center) { - Text("No tasks found.") - } - } - } - items(tasks) { task -> - TaskInfoItem(task) - HorizontalDivider() - } - } - } -} - -@Composable -private fun TaskInfoItem(task: DebugTaskInfo) { - val statusColor = when (task.status) { - "SUCCEEDED" -> Color.Green.copy(alpha = 0.2f) - "FAILED", "CANCELLED" -> Color.Red.copy(alpha = 0.2f) - "RUNNING" -> Color.Yellow.copy(alpha = 0.2f) - "ENQUEUED", "QUEUED" -> Color.Blue.copy(alpha = 0.2f) - else -> Color.Gray.copy(alpha = 0.2f) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(statusColor) - .padding(16.dp) - ) { - Text(text = task.id, style = MaterialTheme.typography.bodySmall, fontSize = 10.sp) - Spacer(Modifier.height(4.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = task.workerClassName, style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f)) - Spacer(Modifier.width(8.dp)) - Text(text = task.status, style = MaterialTheme.typography.bodyMedium) - } - Spacer(Modifier.height(4.dp)) - Row { - Chip(task.type) - if (task.isPeriodic) Chip("Periodic") - if (task.isChain) Chip("Chain") - } - } -} - -@Composable -private fun Chip(text: String) { - Surface( - shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f), - modifier = Modifier.padding(end = 4.dp) - ) { - Text( - text = text, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) - ) - } -} diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugSource.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugSource.kt deleted file mode 100644 index e224e4d..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugSource.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.debug - -/** - * A data class representing the information of a single background task for display on the debug screen. - */ -data class DebugTaskInfo( - val id: String, - val type: String, // e.g., "OneTime", "Periodic", "Chain" - val status: String, // e.g., "ENQUEUED", "RUNNING", "SUCCEEDED" - val workerClassName: String, - val isPeriodic: Boolean = false, - val isChain: Boolean = false -) - -/** - * An interface defining the contract for a platform-specific source - * that can query the list of all known background tasks. - */ -interface DebugSource { - /** - * Asynchronously retrieves a list of all background tasks and their current status. - * @return A list of [DebugTaskInfo] objects. - */ - suspend fun getTasks(): List -} diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugViewModel.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugViewModel.kt deleted file mode 100644 index 71d677e..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/debug/DebugViewModel.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.debug - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class DebugViewModel : KoinComponent { - - private val debugSource: DebugSource by inject() - private val viewModelScope = CoroutineScope(Dispatchers.Main) - - private val _tasks = MutableStateFlow>(emptyList()) - val tasks = _tasks.asStateFlow() - - fun refresh() { - viewModelScope.launch { - _tasks.value = debugSource.getTasks() - } - } -} diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/CommonModule.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/CommonModule.kt deleted file mode 100644 index e098571..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/CommonModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.di - -import dev.brewkits.kmpworkmanager.sample.push.DefaultPushNotificationHandler -import dev.brewkits.kmpworkmanager.sample.push.PushNotificationHandler -import org.koin.dsl.module - -/** - * Koin module containing dependencies shared across all platforms. - */ -val commonModule = module { - // Defines a single instance of PushNotificationHandler using the default implementation. - single { DefaultPushNotificationHandler() } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/Koin.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/Koin.kt deleted file mode 100644 index a6a80ce..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/Koin.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.di - -import org.koin.core.context.startKoin -import org.koin.dsl.KoinAppDeclaration - -/** - * Initializes Koin for targets that only require the common module (e.g., tests, simple previews). - * - * @param appDeclaration Optional lambda to configure the Koin application further. - */ -fun initKoin(appDeclaration: KoinAppDeclaration = {}) { - startKoin { - appDeclaration() - // Include the module containing common dependencies. - modules(commonModule) - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/KoinInitializer.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/KoinInitializer.kt deleted file mode 100644 index c77d2c0..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/di/KoinInitializer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.di - -import org.koin.core.KoinApplication -import org.koin.core.context.startKoin -import org.koin.core.module.Module - -/** - * Advanced Koin initialization function for Multiplatform targets (Android, iOS) - * that require both common and platform-specific dependencies. - * - * @param platformModule The Koin module containing platform-specific implementations. - * @return The initialized KoinApplication instance. - */ -fun initKoin(platformModule: Module): KoinApplication { - return startKoin { - // Load both the shared common dependencies and the platform-specific dependencies. - modules(commonModule, platformModule) - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/DefaultPushNotificationHandler.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/DefaultPushNotificationHandler.kt deleted file mode 100644 index 6944b40..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/DefaultPushNotificationHandler.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.push - -/** - * Default implementation of PushNotificationHandler containing common logic. - * This class handles logging and placeholders for actual server/business logic. - */ -class DefaultPushNotificationHandler : PushNotificationHandler { - /** - * Placeholder implementation for sending the device token to the server. - */ - override fun sendTokenToServer(token: String) { - println(" KMP_PUSH: Received token. Would send to server: $token") - // In a real project, you would call an API service here to send the token. - } - - /** - * Handles and processes the push notification payload. - * This is where shared business logic for push data should reside. - */ - override fun handlePushPayload(payload: Map) { - println(" KMP_PUSH: Received payload. Processing in common code...") - payload.forEach { (key, value) -> - println(" - $key: $value") - } - // Handle common business logic here, e.g., update DB, refresh UI components, etc. - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/FakePushNotificationHandler.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/FakePushNotificationHandler.kt deleted file mode 100644 index 97337d9..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/FakePushNotificationHandler.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.push - -/** - * A fake implementation of PushNotificationHandler for use in previews or unit tests. - * All methods are no-ops (do nothing). - */ -class FakePushNotificationHandler : PushNotificationHandler { - /** - * No-op implementation for sending token to server. - */ - override fun sendTokenToServer(token: String) {} - - /** - * No-op implementation for handling push payload. - */ - override fun handlePushPayload(payload: Map) {} -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/PushNotificationHandler.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/PushNotificationHandler.kt deleted file mode 100644 index 77b86d6..0000000 --- a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/push/PushNotificationHandler.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.brewkits.kmpworkmanager.sample.push - -/** - * Interface defining the necessary methods for handling push notification events - * across different platforms (Android/iOS). - */ -interface PushNotificationHandler { - /** - * Sends the device token (FCM or APNs token) to your backend server for targeting. - * @param token The device token received from FCM or APNs. - */ - fun sendTokenToServer(token: String) - - /** - * Processes the data (payload) received from a push notification. - * This method typically contains the common business logic for push handling. - * @param payload The data map received in the push notification. - */ - fun handlePushPayload(payload: Map) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/DemoWorker.kt b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/DemoWorker.kt new file mode 100644 index 0000000..e601821 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/DemoWorker.kt @@ -0,0 +1,39 @@ +package dev.brewkits.kmpworkmanager.sample.workers + +import dev.brewkits.kmpworkmanager.background.domain.Worker +import kotlinx.coroutines.delay + +/** + * Demo worker for v2.1.0 sample app. + * + * This worker demonstrates: + * - Cross-platform worker implementation + * - Simple async operation (delay simulation) + * - Success/failure handling + */ +class DemoWorker : Worker { + override suspend fun doWork(input: String?): Boolean { + println("โœ… DemoWorker started with input: $input") + + // Simulate work + delay(2000) + + println("โœ… DemoWorker completed successfully") + return true + } +} + +/** + * Heavy processing worker for long-running tasks. + */ +class HeavyWorker : Worker { + override suspend fun doWork(input: String?): Boolean { + println("โš™๏ธ HeavyWorker started (long-running task)") + + // Simulate heavy processing + delay(5000) + + println("โœ… HeavyWorker completed") + return true + } +} diff --git a/composeApp/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/SampleWorkerFactory.kt b/composeApp/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/SampleWorkerFactory.kt new file mode 100644 index 0000000..fc70161 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/sample/workers/SampleWorkerFactory.kt @@ -0,0 +1,47 @@ +package dev.brewkits.kmpworkmanager.sample.workers + +import dev.brewkits.kmpworkmanager.background.data.IosWorker +import dev.brewkits.kmpworkmanager.background.data.IosWorkerFactory + +/** + * Sample WorkerFactory for iOS - v2.1.0 Demo App. + * + * This factory demonstrates how to: + * - Implement IosWorkerFactory for v2.1.0 + * - Register workers by class name + * - Use common worker implementations + */ +class SampleWorkerFactory : IosWorkerFactory { + override fun createWorker(workerClassName: String): IosWorker? { + return when (workerClassName) { + "DemoWorker" -> DemoWorkerIos() + "HeavyWorker" -> HeavyWorkerIos() + else -> { + println("โŒ Unknown worker: $workerClassName") + null + } + } + } +} + +/** + * iOS wrapper for DemoWorker. + */ +private class DemoWorkerIos : IosWorker { + private val worker = DemoWorker() + + override suspend fun doWork(input: String?): Boolean { + return worker.doWork(input) + } +} + +/** + * iOS wrapper for HeavyWorker. + */ +private class HeavyWorkerIos : IosWorker { + private val worker = HeavyWorker() + + override suspend fun doWork(input: String?): Boolean { + return worker.doWork(input) + } +} diff --git a/docs/migration-v2.1.0.md b/docs/migration-v2.1.0.md new file mode 100644 index 0000000..adf2914 --- /dev/null +++ b/docs/migration-v2.1.0.md @@ -0,0 +1,274 @@ +# Migration Guide: v2.0.0 โ†’ v2.1.0 + +## Overview + +**Version 2.1.0** introduces DI-agnostic architecture - Koin is now **optional**. + +### Key Changes +- โœ… Core library has **zero DI framework dependencies** +- โœ… **100% backward compatible** with v2.0.0 (with kmpworker-koin extension) +- โœ… New manual initialization API (no DI framework required) +- โœ… Koin support moved to optional `kmpworkmanager-koin` extension +- โœ… Hilt support available via optional `kmpworkmanager-hilt` extension (Android only) + +--- + +## Migration Paths + +### Option 1: Keep Using Koin (Recommended for Existing Users) + +**Minimal changes** - just add one dependency: + +#### Step 1: Add Koin Extension Dependency + +```kotlin +// build.gradle.kts +dependencies { + implementation("dev.brewkits:kmpworkmanager:2.1.0") + implementation("dev.brewkits:kmpworkmanager-koin:2.1.0") // ADD THIS +} +``` + +#### Step 2: No Code Changes Needed! + +Your existing Koin setup continues to work: + +```kotlin +// Android +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@MyApp) + modules(kmpWorkerModule(MyWorkerFactory())) // Works exactly as before + } + } +} + +// iOS +fun initKoin() { + startKoin { + modules(kmpWorkerModule( + workerFactory = MyWorkerFactory(), + iosTaskIds = setOf("my-task") + )) + } +} +``` + +--- + +### Option 2: Migrate to Manual Initialization (No DI Framework) + +**Best for new projects** or if you don't use Koin elsewhere. + +#### Step 1: Remove Koin Dependencies + +```kotlin +// build.gradle.kts +dependencies { + implementation("dev.brewkits:kmpworkmanager:2.1.0") + // Remove: kmpworkmanager-koin + // Remove: koin-core, koin-android +} +``` + +#### Step 2: Replace Koin Setup with Manual Initialization + +**Android:** + +```kotlin +// Before (v2.0.0 with Koin): +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@MyApp) + modules(kmpWorkerModule(MyWorkerFactory())) + } + } +} + +// After (v2.1.0 manual): +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + WorkerManagerInitializer.initialize( + workerFactory = MyWorkerFactory(), + context = this + ) + } +} +``` + +**iOS:** + +```kotlin +// Before (v2.0.0 with Koin): +fun initKoin() { + startKoin { + modules(kmpWorkerModule( + workerFactory = MyWorkerFactory(), + iosTaskIds = setOf("my-task") + )) + } +} + +// After (v2.1.0 manual): +fun initializeWorkManager() { + WorkerManagerInitializer.initialize( + workerFactory = MyWorkerFactory(), + iosTaskIds = setOf("my-task") + ) +} +``` + +--- + +### Option 3: Migrate to Hilt (Android Only) + +**Best for apps already using Hilt/Dagger.** + +#### Step 1: Add Hilt Extension Dependency + +```kotlin +// build.gradle.kts +dependencies { + implementation("dev.brewkits:kmpworkmanager:2.1.0") + implementation("dev.brewkits:kmpworkmanager-hilt:2.1.0") +} +``` + +#### Step 2: Set Up Hilt Module + +```kotlin +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Provides + @Singleton + fun provideWorkerFactory(): WorkerFactory = MyWorkerFactory() +} + +@HiltAndroidApp +class MyApp : Application() { + @Inject lateinit var initializer: WorkerManagerHiltInitializer + + override fun onCreate() { + super.onCreate() + initializer.initialize() + } +} +``` + +--- + +## Breaking Changes + +**None!** Version 2.1.0 is 100% backward compatible when using `kmpworkmanager-koin` extension. + +--- + +## New APIs + +### WorkerManagerConfig + +Global service locator for WorkerFactory (internal - used by library): + +```kotlin +object WorkerManagerConfig { + fun initialize(factory: WorkerFactory) + fun getWorkerFactory(): WorkerFactory + fun isInitialized(): Boolean + fun reset() // Testing only +} +``` + +### WorkerManagerInitializer + +Unified initialization API: + +```kotlin +expect object WorkerManagerInitializer { + fun initialize( + workerFactory: WorkerFactory, + context: Any? = null, // Android: Context (required) + iosTaskIds: Set = emptySet() // iOS: Task IDs (optional) + ): BackgroundTaskScheduler + + fun getScheduler(): BackgroundTaskScheduler + fun isInitialized(): Boolean + fun reset() // Testing only +} +``` + +--- + +## Troubleshooting + +### "Unresolved reference: kmpWorkerModule" + +**Cause**: Missing `kmpworkmanager-koin` dependency. + +**Solution**: Add the dependency: +```kotlin +implementation("dev.brewkits:kmpworkmanager-koin:2.1.0") +``` + +### "WorkerManagerConfig not initialized" + +**Cause**: Forgot to call `WorkerManagerInitializer.initialize()`. + +**Solution**: Initialize in Application.onCreate() (Android) or AppDelegate (iOS): +```kotlin +WorkerManagerInitializer.initialize( + workerFactory = MyWorkerFactory(), + context = applicationContext // Android only +) +``` + +### "WorkerFactory must implement AndroidWorkerFactory" + +**Cause**: On Android, your factory must implement `AndroidWorkerFactory` (not just `WorkerFactory`). + +**Solution**: +```kotlin +class MyWorkerFactory : AndroidWorkerFactory { // Not just WorkerFactory + override fun createWorker(workerClassName: String): AndroidWorker? { + // ... + } +} +``` + +--- + +## FAQ + +**Q: Do I need to migrate away from Koin?** +A: No! Koin support is fully maintained in the `kmpworkmanager-koin` extension. It's just optional now. + +**Q: Will my existing v2.0.0 code break?** +A: No, as long as you add the `kmpworkmanager-koin:2.1.0` dependency, everything works as before. + +**Q: Should I use manual initialization or Koin?** +A: +- **Use Koin** if: You already use Koin in your app, or prefer DI frameworks +- **Use manual** if: You want zero dependencies, or building a new lightweight project +- **Use Hilt** if: You already use Hilt in your Android app + +**Q: Can I mix manual and Koin initialization?** +A: No, choose one approach per app. The `WorkerManagerConfig` can only be initialized once. + +--- + +## Next Steps + +1. โœ… Choose your migration path +2. โœ… Update dependencies +3. โœ… Update initialization code (if using manual) +4. โœ… Test your app +5. โœ… Enjoy the flexibility! + +For more help, see: +- [README.md](../README.md) +- [GitHub Issues](https://github.com/yourusername/kmpworkmanager/issues) diff --git a/kmpworker-koin/build.gradle.kts b/kmpworker-koin/build.gradle.kts new file mode 100644 index 0000000..673caf2 --- /dev/null +++ b/kmpworker-koin/build.gradle.kts @@ -0,0 +1,108 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + id("maven-publish") + id("signing") +} + +group = "dev.brewkits" +version = "2.1.0" + +kotlin { + androidTarget { + publishLibraryVariants("release") + + mavenPublication { + artifactId = "kmpworkmanager-koin" + } + + compilations.all { + compileTaskProvider.configure { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + } + } + + jvmToolchain(17) + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "KMPWorkManagerKoin" + isStatic = true + } + } + + sourceSets { + commonMain.dependencies { + // Core KMP WorkManager library + api(project(":kmpworker")) + // Koin for dependency injection + implementation(libs.koin.core) + } + + androidMain.dependencies { + // Koin for Android + implementation(libs.koin.android) + } + } +} + +android { + namespace = "dev.brewkits.kmpworkmanager.koin" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +publishing { + publications { + withType { + pom { + name.set("KMP WorkManager Koin Extension") + description.set("Koin dependency injection extension for KMP WorkManager") + url.set("https://github.com/yourusername/kmpworkmanager") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + + developers { + developer { + id.set("brewkits") + name.set("Brewkits Dev") + email.set("dev@brewkits.dev") + } + } + + scm { + connection.set("scm:git:git://github.com/yourusername/kmpworkmanager.git") + developerConnection.set("scm:git:ssh://github.com:yourusername/kmpworkmanager.git") + url.set("https://github.com/yourusername/kmpworkmanager") + } + } + } + } +} + +signing { + useGpgCmd() + sign(publishing.publications) +} diff --git a/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.android.kt b/kmpworker-koin/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.android.kt similarity index 57% rename from kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.android.kt rename to kmpworker-koin/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.android.kt index a9bcfea..2e0af46 100644 --- a/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.android.kt +++ b/kmpworker-koin/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.android.kt @@ -1,19 +1,17 @@ -package dev.brewkits.kmpworkmanager +package dev.brewkits.kmpworkmanager.koin import android.content.Context -import dev.brewkits.kmpworkmanager.background.data.NativeTaskScheduler +import dev.brewkits.kmpworkmanager.WorkerManagerInitializer import dev.brewkits.kmpworkmanager.background.domain.AndroidWorkerFactory import dev.brewkits.kmpworkmanager.background.domain.BackgroundTaskScheduler -import dev.brewkits.kmpworkmanager.background.domain.TaskEventManager import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory -import dev.brewkits.kmpworkmanager.persistence.AndroidEventStore -import dev.brewkits.kmpworkmanager.persistence.EventStore import org.koin.dsl.module /** - * Android implementation of the Koin module. + * Android implementation of the Koin module for KMP WorkManager. * - * v4.0.0+ Breaking Change: Now requires WorkerFactory parameter + * This module uses WorkerManagerInitializer to set up the scheduler, + * maintaining backward compatibility with v2.0.0 while using the new DI-agnostic core. * * Usage: * ```kotlin @@ -27,6 +25,7 @@ import org.koin.dsl.module * * @param workerFactory User-provided factory implementing AndroidWorkerFactory * @param iosTaskIds Ignored on Android (iOS-only parameter) + * @since 2.1.0 */ actual fun kmpWorkerModule( workerFactory: WorkerFactory, @@ -34,24 +33,18 @@ actual fun kmpWorkerModule( ) = module { single { val context = get() - NativeTaskScheduler(context) + + // Use WorkerManagerInitializer to set up everything + WorkerManagerInitializer.initialize( + workerFactory = workerFactory, + context = context + ) } - // Register the user's worker factory + // Register the user's worker factory for direct injection if needed single { workerFactory } single { workerFactory as? AndroidWorkerFactory ?: error("WorkerFactory must implement AndroidWorkerFactory on Android") } - - // Event persistence - single { - val context = get() - val store = AndroidEventStore(context) - - // Initialize TaskEventManager with the store - TaskEventManager.initialize(store) - - store - } } diff --git a/kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.kt b/kmpworker-koin/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.kt similarity index 61% rename from kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.kt rename to kmpworker-koin/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.kt index a9d6eb5..57e1b3a 100644 --- a/kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.kt +++ b/kmpworker-koin/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.kt @@ -1,13 +1,14 @@ -package dev.brewkits.kmpworkmanager +package dev.brewkits.kmpworkmanager.koin import dev.brewkits.kmpworkmanager.background.domain.BackgroundTaskScheduler import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory import org.koin.dsl.module /** - * Koin dependency injection module for KMP WorkManager. + * Koin dependency injection extension for KMP WorkManager. * - * v4.0.0+ Breaking Change: Now requires WorkerFactory parameter + * This extension module provides Koin integration for KMP WorkManager v2.1.0+. + * The core library is now DI-agnostic, and Koin support is provided through this optional extension. * * Usage in your app: * ```kotlin @@ -18,12 +19,17 @@ import org.koin.dsl.module * )) * } * ``` + * + * @since 2.1.0 */ /** * Creates a Koin module for KMP WorkManager with platform-specific scheduler and worker factory. * - * v4.0.0+ Breaking Change: Now requires WorkerFactory parameter + * This module: + * - Initializes WorkerManagerConfig with your WorkerFactory + * - Creates and provides BackgroundTaskScheduler + * - Sets up event persistence (EventStore) * * @param workerFactory User-provided factory for creating worker instances * @param iosTaskIds (iOS only) Additional task IDs for iOS BGTaskScheduler. Ignored on Android. @@ -34,7 +40,10 @@ expect fun kmpWorkerModule( ): org.koin.core.module.Module /** - * Common module definition for direct use (advanced usage) + * Common module definition for direct use (advanced usage). + * + * This is used internally by platform-specific implementations. + * Most users should use kmpWorkerModule() instead. */ fun kmpWorkerCoreModule( scheduler: BackgroundTaskScheduler, diff --git a/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.ios.kt b/kmpworker-koin/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.ios.kt similarity index 72% rename from kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.ios.kt rename to kmpworker-koin/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.ios.kt index 38d8d1f..47acbed 100644 --- a/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/KoinModule.ios.kt +++ b/kmpworker-koin/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/koin/KoinModule.ios.kt @@ -1,18 +1,16 @@ -package dev.brewkits.kmpworkmanager +package dev.brewkits.kmpworkmanager.koin +import dev.brewkits.kmpworkmanager.WorkerManagerInitializer import dev.brewkits.kmpworkmanager.background.data.IosWorkerFactory -import dev.brewkits.kmpworkmanager.background.data.NativeTaskScheduler import dev.brewkits.kmpworkmanager.background.domain.BackgroundTaskScheduler -import dev.brewkits.kmpworkmanager.background.domain.TaskEventManager import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory -import dev.brewkits.kmpworkmanager.persistence.EventStore -import dev.brewkits.kmpworkmanager.persistence.IosEventStore import org.koin.dsl.module /** - * iOS implementation of the Koin module. + * iOS implementation of the Koin module for KMP WorkManager. * - * v4.0.0+ Breaking Change: Now requires WorkerFactory parameter + * This module uses WorkerManagerInitializer to set up the scheduler, + * maintaining backward compatibility with v2.0.0 while using the new DI-agnostic core. * * Usage: * ```kotlin @@ -38,6 +36,7 @@ import org.koin.dsl.module * * @param workerFactory User-provided factory implementing IosWorkerFactory * @param iosTaskIds Additional iOS task IDs (optional, Info.plist is primary source) + * @since 2.1.0 */ actual fun kmpWorkerModule( workerFactory: WorkerFactory, @@ -69,20 +68,14 @@ actual fun kmpWorkerModule( } single { - NativeTaskScheduler(additionalPermittedTaskIds = iosTaskIds) + // Use WorkerManagerInitializer to set up everything + WorkerManagerInitializer.initialize( + workerFactory = workerFactory, + iosTaskIds = iosTaskIds + ) } - // Register the user's worker factory (already validated above) + // Register the user's worker factory for direct injection if needed single { workerFactory } single { workerFactory } - - // Event persistence - single { - val store = IosEventStore() - - // Initialize TaskEventManager with the store - TaskEventManager.initialize(store) - - store - } } diff --git a/kmpworker/build.gradle.kts b/kmpworker/build.gradle.kts index 13e4160..b836664 100644 --- a/kmpworker/build.gradle.kts +++ b/kmpworker/build.gradle.kts @@ -13,12 +13,16 @@ plugins { } group = "dev.brewkits" -version = "2.0.0" +version = "2.1.0" kotlin { androidTarget { publishLibraryVariants("release") + mavenPublication { + artifactId = "kmpworkmanager-android" + } + compilations.all { compileTaskProvider.configure { compilerOptions { @@ -47,13 +51,9 @@ kotlin { implementation(libs.androidx.work.runtime.ktx) // Coroutines support for Guava ListenableFuture implementation(libs.kotlinx.coroutines.guava) - // Koin for Android - implementation(libs.koin.android) } commonMain.dependencies { - // Koin for dependency injection - implementation(libs.koin.core) // Kotlinx Datetime for handling dates and times implementation(libs.kotlinx.datetime) // Kotlinx Serialization for JSON processing @@ -88,6 +88,7 @@ publishing { // Configure all publications with common POM information withType { groupId = "dev.brewkits" + artifactId = artifactId.replace("kmpworker", "kmpworkmanager") version = "2.0.0" pom { diff --git a/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/AndroidWorkerFactoryProvider.kt b/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/AndroidWorkerFactoryProvider.kt new file mode 100644 index 0000000..3e4453e --- /dev/null +++ b/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/AndroidWorkerFactoryProvider.kt @@ -0,0 +1,34 @@ +package dev.brewkits.kmpworkmanager + +import dev.brewkits.kmpworkmanager.background.domain.AndroidWorkerFactory +import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory + +/** + * Platform-specific accessor for retrieving the AndroidWorkerFactory on Android. + * + * This ensures type safety - the registered WorkerFactory must implement AndroidWorkerFactory + * on the Android platform. + * + * Internal usage only - used by KmpWorker and KmpHeavyWorker. + * + * @since 2.1.0 + */ +object AndroidWorkerFactoryProvider { + /** + * Retrieves the Android-specific WorkerFactory. + * + * @return The registered AndroidWorkerFactory instance + * @throws IllegalStateException if WorkerManagerConfig is not initialized + * @throws IllegalArgumentException if the registered factory is not an AndroidWorkerFactory + */ + fun getAndroidWorkerFactory(): AndroidWorkerFactory { + val factory: WorkerFactory = WorkerManagerConfig.getWorkerFactory() + + require(factory is AndroidWorkerFactory) { + "On Android, WorkerFactory must implement AndroidWorkerFactory. " + + "Found: ${factory::class.simpleName}" + } + + return factory + } +} diff --git a/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.android.kt b/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.android.kt new file mode 100644 index 0000000..f4d35b5 --- /dev/null +++ b/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.android.kt @@ -0,0 +1,73 @@ +package dev.brewkits.kmpworkmanager + +import android.content.Context +import dev.brewkits.kmpworkmanager.background.data.NativeTaskScheduler +import dev.brewkits.kmpworkmanager.background.domain.BackgroundTaskScheduler +import dev.brewkits.kmpworkmanager.background.domain.TaskEventManager +import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory +import dev.brewkits.kmpworkmanager.persistence.AndroidEventStore +import dev.brewkits.kmpworkmanager.persistence.EventStore +import kotlin.concurrent.Volatile + +/** + * Android implementation of WorkerManagerInitializer. + * + * Initializes: + * 1. WorkerManagerConfig with the provided WorkerFactory + * 2. AndroidEventStore for task event persistence + * 3. TaskEventManager for event handling + * 4. NativeTaskScheduler for background task scheduling + * + * @since 2.1.0 + */ +actual object WorkerManagerInitializer { + @Volatile + private var scheduler: BackgroundTaskScheduler? = null + + @Volatile + private var eventStore: EventStore? = null + + actual fun initialize( + workerFactory: WorkerFactory, + context: Any?, + iosTaskIds: Set + ): BackgroundTaskScheduler { + check(scheduler == null) { + "WorkerManagerInitializer already initialized. Call reset() first if re-initialization is needed." + } + + require(context is Context) { + "Android requires Context parameter. " + + "Usage: WorkerManagerInitializer.initialize(factory, context = applicationContext)" + } + + // 1. Register factory globally + WorkerManagerConfig.initialize(workerFactory) + + // 2. Initialize EventStore for task event persistence + val store = AndroidEventStore(context) + TaskEventManager.initialize(store) + eventStore = store + + // 3. Create and cache scheduler + val nativeScheduler = NativeTaskScheduler(context) + scheduler = nativeScheduler + + return nativeScheduler + } + + actual fun getScheduler(): BackgroundTaskScheduler { + return scheduler ?: error( + "WorkerManagerInitializer not initialized. " + + "Call WorkerManagerInitializer.initialize(factory, context) first." + ) + } + + actual fun isInitialized(): Boolean = scheduler != null + + actual fun reset() { + scheduler = null + eventStore = null + WorkerManagerConfig.reset() + } +} diff --git a/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpHeavyWorker.kt b/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpHeavyWorker.kt index c5ac4b5..c3aacdc 100644 --- a/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpHeavyWorker.kt +++ b/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpHeavyWorker.kt @@ -8,11 +8,10 @@ import androidx.core.app.NotificationCompat import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters +import dev.brewkits.kmpworkmanager.AndroidWorkerFactoryProvider import dev.brewkits.kmpworkmanager.background.domain.AndroidWorkerFactory import dev.brewkits.kmpworkmanager.utils.Logger import dev.brewkits.kmpworkmanager.utils.LogTags -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject /** * Heavy worker that runs in foreground service with persistent notification. @@ -50,9 +49,11 @@ import org.koin.core.component.inject class KmpHeavyWorker( appContext: Context, workerParams: WorkerParameters -) : CoroutineWorker(appContext, workerParams), KoinComponent { +) : CoroutineWorker(appContext, workerParams) { - private val workerFactory: AndroidWorkerFactory by inject() + private val workerFactory: AndroidWorkerFactory by lazy { + AndroidWorkerFactoryProvider.getAndroidWorkerFactory() + } companion object { const val CHANNEL_ID = "kmp_heavy_worker_channel" @@ -139,7 +140,8 @@ class KmpHeavyWorker( /** * Executes the actual heavy work by delegating to the specified worker class. * - * v1.0.0+: Now uses AndroidWorkerFactory from Koin + * v2.1.0+: Uses AndroidWorkerFactoryProvider to retrieve factory from WorkerManagerConfig + * (replaces Koin dependency) * * @param workerClassName Fully qualified worker class name * @param inputJson Optional JSON input data diff --git a/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpWorker.kt b/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpWorker.kt index dc28daf..4a331b6 100644 --- a/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpWorker.kt +++ b/kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpWorker.kt @@ -3,31 +3,33 @@ package dev.brewkits.kmpworkmanager.background.data import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import dev.brewkits.kmpworkmanager.AndroidWorkerFactoryProvider import dev.brewkits.kmpworkmanager.background.domain.AndroidWorkerFactory import dev.brewkits.kmpworkmanager.background.domain.TaskCompletionEvent import dev.brewkits.kmpworkmanager.background.domain.TaskEventBus import dev.brewkits.kmpworkmanager.utils.LogTags import dev.brewkits.kmpworkmanager.utils.Logger -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject /** * A generic CoroutineWorker that delegates to user-provided AndroidWorker implementations. * - * v4.0.0+: Uses AndroidWorkerFactory from Koin instead of hardcoded when() statement + * v2.1.0+: Uses AndroidWorkerFactoryProvider to retrieve factory from WorkerManagerConfig + * (replaces Koin dependency) * * This worker acts as the entry point for all deferrable tasks and: * - Retrieves the worker class name from input data - * - Uses the injected AndroidWorkerFactory to create the worker instance + * - Uses the AndroidWorkerFactory to create the worker instance * - Delegates execution to the worker's doWork() method * - Emits events to TaskEventBus for UI updates */ class KmpWorker( appContext: Context, workerParams: WorkerParameters -) : CoroutineWorker(appContext, workerParams), KoinComponent { +) : CoroutineWorker(appContext, workerParams) { - private val workerFactory: AndroidWorkerFactory by inject() + private val workerFactory: AndroidWorkerFactory by lazy { + AndroidWorkerFactoryProvider.getAndroidWorkerFactory() + } override suspend fun doWork(): Result { val workerClassName = inputData.getString("workerClassName") ?: return Result.failure() diff --git a/kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerConfig.kt b/kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerConfig.kt new file mode 100644 index 0000000..4982b20 --- /dev/null +++ b/kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerConfig.kt @@ -0,0 +1,78 @@ +package dev.brewkits.kmpworkmanager + +import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory +import kotlin.concurrent.Volatile + +/** + * Global configuration singleton for KMP WorkManager. + * + * This provides a DI-agnostic service locator pattern for registering the WorkerFactory. + * Eliminates the need for Koin or any other DI framework in the core library. + * + * Usage (Manual Registration): + * ```kotlin + * // In Application.onCreate() or AppDelegate + * WorkerManagerConfig.initialize(MyWorkerFactory()) + * ``` + * + * Usage (With DI): + * ```kotlin + * // DI extensions (koin/hilt) will call initialize() automatically + * // You don't need to call this directly + * ``` + * + * @since 2.1.0 + */ +object WorkerManagerConfig { + @Volatile + private var workerFactory: WorkerFactory? = null + + /** + * Initializes the global WorkerFactory. + * + * Must be called once before scheduling any tasks from the main thread. + * + * **Thread Safety**: This method should be called from a single thread (typically main/UI thread) + * during app initialization. The @Volatile annotation ensures visibility across threads after initialization. + * + * @param factory The WorkerFactory implementation to use + * @throws IllegalStateException if already initialized + */ + fun initialize(factory: WorkerFactory) { + check(workerFactory == null) { + "WorkerManagerConfig already initialized. Call reset() first if re-initialization is needed." + } + workerFactory = factory + } + + /** + * Retrieves the global WorkerFactory instance. + * + * @return The registered WorkerFactory + * @throws IllegalStateException if not initialized + */ + fun getWorkerFactory(): WorkerFactory { + return workerFactory ?: error( + "WorkerManagerConfig not initialized. " + + "Call WorkerManagerConfig.initialize(factory) or use a DI extension module " + + "(kmpworkmanager-koin or kmpworkmanager-hilt)." + ) + } + + /** + * Checks if the WorkerFactory has been initialized. + * + * @return true if initialized, false otherwise + */ + fun isInitialized(): Boolean = workerFactory != null + + /** + * Resets the configuration, allowing re-initialization. + * + * **Warning**: This is intended for testing only. Do not call in production code. + * Calling this while tasks are running may cause undefined behavior. + */ + fun reset() { + workerFactory = null + } +} diff --git a/kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.kt b/kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.kt new file mode 100644 index 0000000..b7545da --- /dev/null +++ b/kmpworker/src/commonMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.kt @@ -0,0 +1,86 @@ +package dev.brewkits.kmpworkmanager + +import dev.brewkits.kmpworkmanager.background.domain.BackgroundTaskScheduler +import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory + +/** + * Platform-specific initializer for KMP WorkManager. + * + * This provides a unified API for initializing the library on both Android and iOS + * without requiring any DI framework. + * + * Usage (Android): + * ```kotlin + * class MyApp : Application() { + * override fun onCreate() { + * super.onCreate() + * val scheduler = WorkerManagerInitializer.initialize( + * workerFactory = MyWorkerFactory(), + * context = this + * ) + * } + * } + * ``` + * + * Usage (iOS): + * ```kotlin + * // In AppDelegate + * fun initializeWorkManager() { + * WorkerManagerInitializer.initialize( + * workerFactory = MyWorkerFactory(), + * iosTaskIds = setOf("kmp-sync-task", "kmp-upload-task") + * ) + * } + * ``` + * + * @since 2.1.0 + */ +expect object WorkerManagerInitializer { + /** + * Initializes KMP WorkManager with the provided WorkerFactory. + * + * **Android Parameters:** + * - `context`: Application context (required) + * - `workerFactory`: WorkerFactory implementation (must implement AndroidWorkerFactory) + * + * **iOS Parameters:** + * - `workerFactory`: WorkerFactory implementation (must implement IosWorkerFactory) + * - `iosTaskIds`: Set of task identifiers for background tasks (optional if defined in Info.plist) + * + * Thread-safe and idempotent - subsequent calls will throw an exception. + * + * @param workerFactory The WorkerFactory implementation to use + * @param context Platform-specific context (Android only) + * @param iosTaskIds Set of iOS task identifiers (iOS only, optional) + * @return BackgroundTaskScheduler instance for scheduling tasks + * @throws IllegalStateException if already initialized + */ + fun initialize( + workerFactory: WorkerFactory, + context: Any? = null, + iosTaskIds: Set = emptySet() + ): BackgroundTaskScheduler + + /** + * Retrieves the initialized BackgroundTaskScheduler. + * + * @return The scheduler instance + * @throws IllegalStateException if not initialized + */ + fun getScheduler(): BackgroundTaskScheduler + + /** + * Checks if WorkerManager has been initialized. + * + * @return true if initialized, false otherwise + */ + fun isInitialized(): Boolean + + /** + * Resets the initializer state, allowing re-initialization. + * + * **Warning**: This is intended for testing only. Do not call in production code. + * Calling this while tasks are running may cause undefined behavior. + */ + fun reset() +} diff --git a/kmpworker/src/commonTest/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerConfigTest.kt b/kmpworker/src/commonTest/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerConfigTest.kt new file mode 100644 index 0000000..6086a3d --- /dev/null +++ b/kmpworker/src/commonTest/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerConfigTest.kt @@ -0,0 +1,102 @@ +package dev.brewkits.kmpworkmanager + +import dev.brewkits.kmpworkmanager.background.domain.Worker +import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory +import kotlin.test.* + +/** + * Unit tests for WorkerManagerConfig. + * + * Tests the DI-agnostic service locator pattern for WorkerFactory registration. + */ +class WorkerManagerConfigTest { + + private class TestWorkerFactory : WorkerFactory { + override fun createWorker(workerClassName: String): Worker? = null + } + + @BeforeTest + fun setup() { + // Reset before each test to ensure clean state + WorkerManagerConfig.reset() + } + + @AfterTest + fun teardown() { + // Clean up after each test + WorkerManagerConfig.reset() + } + + @Test + fun `initialize sets factory successfully`() { + val factory = TestWorkerFactory() + + WorkerManagerConfig.initialize(factory) + + assertTrue(WorkerManagerConfig.isInitialized()) + assertSame(factory, WorkerManagerConfig.getWorkerFactory()) + } + + @Test + fun `initialize throws when already initialized`() { + val factory1 = TestWorkerFactory() + val factory2 = TestWorkerFactory() + + WorkerManagerConfig.initialize(factory1) + + assertFailsWith { + WorkerManagerConfig.initialize(factory2) + } + } + + @Test + fun `getWorkerFactory throws when not initialized`() { + assertFalse(WorkerManagerConfig.isInitialized()) + + assertFailsWith { + WorkerManagerConfig.getWorkerFactory() + } + } + + @Test + fun `isInitialized returns false before initialization`() { + assertFalse(WorkerManagerConfig.isInitialized()) + } + + @Test + fun `isInitialized returns true after initialization`() { + WorkerManagerConfig.initialize(TestWorkerFactory()) + + assertTrue(WorkerManagerConfig.isInitialized()) + } + + @Test + fun `reset allows re-initialization`() { + val factory1 = TestWorkerFactory() + val factory2 = TestWorkerFactory() + + // First initialization + WorkerManagerConfig.initialize(factory1) + assertSame(factory1, WorkerManagerConfig.getWorkerFactory()) + + // Reset and re-initialize + WorkerManagerConfig.reset() + assertFalse(WorkerManagerConfig.isInitialized()) + + WorkerManagerConfig.initialize(factory2) + assertSame(factory2, WorkerManagerConfig.getWorkerFactory()) + } + + @Test + fun `reset clears initialization state`() { + WorkerManagerConfig.initialize(TestWorkerFactory()) + assertTrue(WorkerManagerConfig.isInitialized()) + + WorkerManagerConfig.reset() + + assertFalse(WorkerManagerConfig.isInitialized()) + assertFailsWith { + WorkerManagerConfig.getWorkerFactory() + } + } +} diff --git a/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/IosTaskHandlerRegistry.kt b/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/IosTaskHandlerRegistry.kt new file mode 100644 index 0000000..6dbef69 --- /dev/null +++ b/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/IosTaskHandlerRegistry.kt @@ -0,0 +1,108 @@ +package dev.brewkits.kmpworkmanager + +import dev.brewkits.kmpworkmanager.background.data.ChainExecutor +import dev.brewkits.kmpworkmanager.background.data.SingleTaskExecutor +import dev.brewkits.kmpworkmanager.utils.Logger +import dev.brewkits.kmpworkmanager.utils.LogTags +import platform.BackgroundTasks.BGTask + +/** + * Registry for iOS task handlers (ChainExecutor and SingleTaskExecutor). + * + * This provides a DI-agnostic way to access task executors on iOS, using lazy initialization + * with the WorkerFactory from WorkerManagerConfig. + * + * Usage (in Swift AppDelegate): + * ```swift + * func handleBackgroundTask(_ task: BGTask) { + * // Kotlin code will access executors via IosTaskHandlerRegistry + * IosTaskHandlerRegistryKt.handleTask(task: task, taskId: task.identifier) + * } + * ``` + * + * Note: This replaces Koin-based injection for ChainExecutor and SingleTaskExecutor. + * + * @since 2.1.0 + */ +object IosTaskHandlerRegistry { + /** + * Lazy-initialized ChainExecutor. + * + * Created on first access using the WorkerFactory from WorkerManagerConfig. + * Thread-safe via lazy delegation. + */ + private val chainExecutor: ChainExecutor by lazy { + val factory = IosWorkerFactoryProvider.getIosWorkerFactory() + Logger.i(LogTags.SCHEDULER, "IosTaskHandlerRegistry: Creating ChainExecutor") + ChainExecutor(workerFactory = factory) + } + + /** + * Lazy-initialized SingleTaskExecutor. + * + * Created on first access using the WorkerFactory from WorkerManagerConfig. + * Thread-safe via lazy delegation. + */ + private val singleTaskExecutor: SingleTaskExecutor by lazy { + val factory = IosWorkerFactoryProvider.getIosWorkerFactory() + Logger.i(LogTags.SCHEDULER, "IosTaskHandlerRegistry: Creating SingleTaskExecutor") + SingleTaskExecutor(workerFactory = factory) + } + + /** + * Retrieves the ChainExecutor instance. + * + * @return ChainExecutor for handling task chains + * @throws IllegalStateException if WorkerManagerConfig is not initialized + */ + fun getChainExecutor(): ChainExecutor = chainExecutor + + /** + * Retrieves the SingleTaskExecutor instance. + * + * @return SingleTaskExecutor for handling single tasks + * @throws IllegalStateException if WorkerManagerConfig is not initialized + */ + fun getSingleTaskExecutor(): SingleTaskExecutor = singleTaskExecutor + + /** + * Handles an iOS BGTask using the appropriate executor. + * + * This is a convenience method that can be called directly from Swift/Objective-C + * to handle background tasks. + * + * @param task The BGTask to handle + * @param taskId The task identifier + */ + suspend fun handleTask(task: BGTask, taskId: String) { + Logger.i(LogTags.SCHEDULER, "IosTaskHandlerRegistry: Handling task $taskId") + + when { + taskId.contains("chain", ignoreCase = true) -> { + Logger.d(LogTags.SCHEDULER, "Task $taskId identified as chain task") + val executor = getChainExecutor() + executor.executeNextChainFromQueue() + } + else -> { + Logger.d(LogTags.SCHEDULER, "Task $taskId identified as single task") + val executor = getSingleTaskExecutor() + // Single task execution logic would go here + // This is just a skeleton - actual implementation depends on task metadata + Logger.w(LogTags.SCHEDULER, "Single task execution not implemented in handleTask()") + } + } + } + + /** + * Resets the registry, clearing all cached executors. + * + * **Warning**: This is intended for testing only. Do not call in production code. + * Calling this while tasks are running may cause undefined behavior. + */ + @Suppress("unused") // Used in tests + fun reset() { + // Note: Cannot actually reset lazy delegates without reflection + // This method is a placeholder for future implementation + Logger.w(LogTags.SCHEDULER, "IosTaskHandlerRegistry: reset() called but lazy delegates cannot be reset") + } +} diff --git a/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/IosWorkerFactoryProvider.kt b/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/IosWorkerFactoryProvider.kt new file mode 100644 index 0000000..8ef9012 --- /dev/null +++ b/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/IosWorkerFactoryProvider.kt @@ -0,0 +1,34 @@ +package dev.brewkits.kmpworkmanager + +import dev.brewkits.kmpworkmanager.background.data.IosWorkerFactory +import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory + +/** + * Platform-specific accessor for retrieving the IosWorkerFactory on iOS. + * + * This ensures type safety - the registered WorkerFactory must implement IosWorkerFactory + * on the iOS platform. + * + * Internal usage only - used by ChainExecutor and SingleTaskExecutor. + * + * @since 2.1.0 + */ +object IosWorkerFactoryProvider { + /** + * Retrieves the iOS-specific WorkerFactory. + * + * @return The registered IosWorkerFactory instance + * @throws IllegalStateException if WorkerManagerConfig is not initialized + * @throws IllegalArgumentException if the registered factory is not an IosWorkerFactory + */ + fun getIosWorkerFactory(): IosWorkerFactory { + val factory: WorkerFactory = WorkerManagerConfig.getWorkerFactory() + + require(factory is IosWorkerFactory) { + "On iOS, WorkerFactory must implement IosWorkerFactory. " + + "Found: ${factory::class.simpleName}" + } + + return factory + } +} diff --git a/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.ios.kt b/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.ios.kt new file mode 100644 index 0000000..7f8b78a --- /dev/null +++ b/kmpworker/src/iosMain/kotlin/dev/brewkits/kmpworkmanager/WorkerManagerInitializer.ios.kt @@ -0,0 +1,69 @@ +package dev.brewkits.kmpworkmanager + +import dev.brewkits.kmpworkmanager.background.data.NativeTaskScheduler +import dev.brewkits.kmpworkmanager.background.domain.BackgroundTaskScheduler +import dev.brewkits.kmpworkmanager.background.domain.TaskEventManager +import dev.brewkits.kmpworkmanager.background.domain.WorkerFactory +import dev.brewkits.kmpworkmanager.persistence.EventStore +import dev.brewkits.kmpworkmanager.persistence.IosEventStore +import kotlin.concurrent.Volatile + +/** + * iOS implementation of WorkerManagerInitializer. + * + * Initializes: + * 1. WorkerManagerConfig with the provided WorkerFactory + * 2. IosEventStore for task event persistence + * 3. TaskEventManager for event handling + * 4. NativeTaskScheduler for background task scheduling + * + * @since 2.1.0 + */ +actual object WorkerManagerInitializer { + @Volatile + private var scheduler: BackgroundTaskScheduler? = null + + @Volatile + private var eventStore: EventStore? = null + + actual fun initialize( + workerFactory: WorkerFactory, + context: Any?, + iosTaskIds: Set + ): BackgroundTaskScheduler { + check(scheduler == null) { + "WorkerManagerInitializer already initialized. Call reset() first if re-initialization is needed." + } + + // 1. Register factory globally + WorkerManagerConfig.initialize(workerFactory) + + // 2. Initialize EventStore for task event persistence + val store = IosEventStore() + TaskEventManager.initialize(store) + eventStore = store + + // 3. Create and cache scheduler + val nativeScheduler = NativeTaskScheduler( + additionalPermittedTaskIds = iosTaskIds + ) + scheduler = nativeScheduler + + return nativeScheduler + } + + actual fun getScheduler(): BackgroundTaskScheduler { + return scheduler ?: error( + "WorkerManagerInitializer not initialized. " + + "Call WorkerManagerInitializer.initialize(factory, iosTaskIds = ...) first." + ) + } + + actual fun isInitialized(): Boolean = scheduler != null + + actual fun reset() { + scheduler = null + eventStore = null + WorkerManagerConfig.reset() + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 6c75e7d..54c8ac5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,4 +30,5 @@ dependencyResolutionManagement { } include(":kmpworker") +include(":kmpworker-koin") include(":composeApp") \ No newline at end of file