├── .gitignore ├── ControlFlow ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── codestarx │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── io │ │ └── github │ │ └── codestarx │ │ ├── ControlFlow.kt │ │ ├── helper │ │ ├── AnyExtensions.kt │ │ ├── Dispatcher.kt │ │ └── HttpException.kt │ │ ├── interfaces │ │ ├── RollbackStatusTracker.kt │ │ ├── RollbackTaskProcessor.kt │ │ ├── TaskProcessor.kt │ │ ├── TaskStatusTracker.kt │ │ └── WorkFlowTracker.kt │ │ ├── models │ │ ├── ConditionData.kt │ │ ├── RetryStrategy.kt │ │ ├── RollbackInfo.kt │ │ ├── TaskFlow.kt │ │ ├── TaskInfo.kt │ │ └── TransformData.kt │ │ └── status │ │ ├── State.kt │ │ └── TaskStatus.kt │ └── test │ └── java │ └── io │ └── github │ └── codestarx │ ├── ControlFlowTest.kt │ ├── DispatcherTest.kt │ └── core │ └── BaseUnitTest.kt ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── github │ │ └── codestarx │ │ └── controlflowdevelop │ │ └── MainActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── attach └── attach.xml ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows thumbnail db 2 | Thumbs.db 3 | 4 | # OSX files 5 | .DS_Store 6 | 7 | # built application files 8 | *.apk 9 | *.ap_ 10 | 11 | # files for the dex VM 12 | *.dex 13 | 14 | # Java class files 15 | *.class 16 | 17 | # generated files 18 | bin/ 19 | gen/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Eclipse project files 26 | .classpath 27 | .project 28 | 29 | # Android Studio 30 | .idea 31 | .gradle 32 | /*/local.properties 33 | /*/out 34 | /*/*/build 35 | /*/*/*/build 36 | build 37 | /*/*/production 38 | /*/*/*/production 39 | *.iml 40 | *.iws 41 | *.ipr 42 | *~ 43 | *.swp 44 | -------------------------------------------------------------------------------- /ControlFlow/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ControlFlow/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | id("com.android.library") 5 | id("org.jetbrains.kotlin.android") 6 | id("com.vanniktech.maven.publish") version "0.26.0" 7 | } 8 | 9 | android { 10 | namespace = "io.github.codestarx" 11 | compileSdk = 34 12 | 13 | defaultConfig { 14 | minSdk = 16 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles("consumer-rules.pro") 17 | } 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_17 29 | targetCompatibility = JavaVersion.VERSION_17 30 | } 31 | kotlinOptions { 32 | jvmTarget = "17" 33 | } 34 | } 35 | 36 | dependencies { 37 | 38 | val coroutine = "1.7.3" 39 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine") 40 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine") 41 | 42 | val lifecycle = "2.7.0-rc02" 43 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle") 44 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle") 45 | 46 | val hamcrest = "2.2" 47 | testImplementation("org.hamcrest:hamcrest-library:$hamcrest") 48 | 49 | val mockk = "1.12.1" 50 | testImplementation("io.mockk:mockk:$mockk") 51 | 52 | val testRules = "1.4.0" 53 | testImplementation("androidx.test:rules:$testRules") 54 | 55 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine") 56 | testImplementation("junit:junit:4.13.2") 57 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 58 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 59 | } 60 | mavenPublishing{ 61 | coordinates("io.github.codestarx", "control-flow", "1.0.0-alpha11") 62 | 63 | publishToMavenCentral(SonatypeHost.S01) 64 | pom { 65 | name.set("control-flow") 66 | description.set("control-flow is an Android library that facilitates task sequencing, rollback actions, and error handling. It systematically oversees task execution, offering structured error handling and facilitating rollback processes for efficient management.") 67 | inceptionYear.set("2024") 68 | url.set("https://github.com/CodeStarX/ControlFlow/") 69 | packaging = "aar" 70 | licenses { 71 | license { 72 | name.set("The Apache License, Version 2.0") 73 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 74 | distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 75 | } 76 | } 77 | developers { 78 | developer { 79 | id.set("xxx") 80 | name.set("Mohsen Soltanian") 81 | url.set("https://github.com/CodeStarX/") 82 | email.set("soltaniyan.mohsen@gmail.com") 83 | } 84 | } 85 | scm { 86 | url.set("https://github.com/CodeStarX/ControlFlow/") 87 | connection.set("scm:git:git://github.com/CodeStarX/ControlFlow.git") 88 | developerConnection.set("scm:git:ssh://git@github.com/CodeStarX/ControlFlow.git") 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /ControlFlow/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/ControlFlow/consumer-rules.pro -------------------------------------------------------------------------------- /ControlFlow/gradle.properties: -------------------------------------------------------------------------------- 1 | RELEASE_SIGNING_ENABLED=true 2 | signing.keyId=xxx 3 | signing.password=xxx 4 | signing.secretKeyRingFile=xxx -------------------------------------------------------------------------------- /ControlFlow/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /ControlFlow/src/androidTest/java/io/github/codestarx/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.controlflow.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/ControlFlow.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx 2 | 3 | import android.util.Log 4 | import androidx.annotation.Keep 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import io.github.codestarx.interfaces.WorkFlowTracker 8 | import io.github.codestarx.interfaces.TaskProcessor 9 | import io.github.codestarx.interfaces.RollbackTaskProcessor 10 | import io.github.codestarx.interfaces.RollbackStatusTracker 11 | import io.github.codestarx.interfaces.TaskStatusTracker 12 | import io.github.codestarx.models.TaskFlow 13 | import io.github.codestarx.status.State 14 | import io.github.codestarx.status.TaskStatus 15 | import kotlinx.coroutines.CoroutineExceptionHandler 16 | import kotlinx.coroutines.CoroutineScope 17 | import kotlinx.coroutines.Job 18 | import kotlinx.coroutines.delay 19 | import kotlinx.coroutines.flow.catch 20 | import kotlinx.coroutines.flow.onStart 21 | import kotlinx.coroutines.flow.retryWhen 22 | import kotlinx.coroutines.isActive 23 | import kotlinx.coroutines.launch 24 | import kotlinx.coroutines.withContext 25 | import java.util.concurrent.atomic.AtomicReference 26 | 27 | /** 28 | * ControlFlow manages the execution sequence of tasks and potential rollback actions. 29 | * It orchestrates the execution, rollback, completion, and error handling of tasks and their rollbacks. 30 | * This class offers a structured way to manage a series of tasks and handles their execution flow and potential rollbacks. 31 | * Example usage: 32 | * 33 | * // Create a ControlFlow instance 34 | * val controlFlow = ControlFlow(object : WorkFlowTracker { 35 | * // Implement work Flow callback methods 36 | * }) 37 | * 38 | * // Define task sequence 39 | * controlFlow.startWith(Task1()) 40 | * controlFlow.then(Task2()) 41 | * 42 | * // Set up callbacks for task and rollback task execution progress 43 | * controlFlow.useTaskStatusTracker(object : TaskStatusTracker { 44 | * // Implement callback methods 45 | * }) 46 | * controlFlow.useRollbackStatusTracker(object : RollbackStatusTracker { 47 | * // Implement rollback callback methods 48 | * }) 49 | * 50 | * // Start executing tasks 51 | * controlFlow.start() 52 | * 53 | * // If an error occurs during execution, initiate rollback manually 54 | * controlFlow.startRollback() 55 | * 56 | * // Restart execution from a specific task or task index 57 | * controlFlow.startFrom(taskName = "Task2") 58 | * controlFlow.startFrom(taskIndex = 1) 59 | * 60 | * // Restart the entire sequence 61 | * controlFlow.restart() 62 | */ 63 | @Keep 64 | class ControlFlow( 65 | private var workFlowTracker: WorkFlowTracker? 66 | ): ViewModel() { 67 | 68 | private val handler = CoroutineExceptionHandler { _, exception -> 69 | // Handles exceptions that occur within coroutines 70 | exception.message?.let { Log.e("ControlFlow", it) } 71 | } 72 | 73 | // Manages the execution of tasks within a coroutine scope 74 | private fun safeLauncher(block: suspend CoroutineScope.() -> Unit): Job { 75 | return viewModelScope.launch(handler, block = block) 76 | } 77 | 78 | // Stores the original sequence of tasks 79 | private var originalTasks: MutableList? = mutableListOf() 80 | 81 | 82 | // Stores the original sequence of rollback tasks 83 | private var originalRollbackTasks: MutableList? = mutableListOf() 84 | 85 | // Stores the tasks to be executed 86 | private var tasks: MutableList? = mutableListOf() 87 | 88 | // Stores the rollback tasks to be executed 89 | private var rollbackTasks: MutableList? = mutableListOf() 90 | 91 | // Stores the completed tasks during execution 92 | private var _completedTasks: MutableList? = mutableListOf() 93 | 94 | // Provides access to the list of completed tasks 95 | val completedTasks: List 96 | get() { return _completedTasks?.toList() ?: listOf() } 97 | 98 | // Stores the completed rollback tasks during execution 99 | private var _completedRollbackTasks: MutableList? = mutableListOf() 100 | 101 | // Provides access to the list of completed rollback tasks 102 | val completedRollbackTasks: List 103 | get() { return _completedRollbackTasks?.toList() ?: listOf() } 104 | 105 | // Tracks if the rollback should be automatically initiated 106 | private var runAutomaticallyRollback: AtomicReference? = null 107 | 108 | // Callback for the main task execution progress 109 | private var taskStatusTracker: TaskStatusTracker? = null 110 | 111 | // Callback for the rollback task execution progress 112 | private var rollbackStatusTracker: RollbackStatusTracker? = null 113 | 114 | // Coroutine job for executing tasks 115 | private var taskJob: Job? = null 116 | 117 | // Coroutine job for executing rollback tasks 118 | private var rollbackTaskJob: Job? = null 119 | 120 | private var taskResult: Any? = null 121 | 122 | private var taskIsCurrentlyInProgress: Boolean? = null 123 | 124 | /** 125 | * Add the first task to the control flow sequence. 126 | * @param first task to be added to the control flow sequence. 127 | */ 128 | fun startWith(first: TaskProcessor) { 129 | originalTasks?.clear() 130 | tasks?.clear() 131 | originalTasks?.add(first) 132 | tasks?.add(first) 133 | if(TaskProcessor.subtasks?.containsKey(first.hashCode()) == true){ 134 | originalTasks?.addAll(TaskProcessor.subtasks!![first.hashCode()] ?: mutableListOf()) 135 | tasks?.addAll(TaskProcessor.subtasks!![first.hashCode()] ?: mutableListOf()) 136 | } 137 | } 138 | 139 | /** 140 | * Adds the next task to the control flow sequence. 141 | * @param next The subsequent task to be added to the control flow sequence. 142 | */ 143 | fun then(next: TaskProcessor) { 144 | originalTasks?.add(next) 145 | tasks?.add(next) 146 | if(TaskProcessor.subtasks?.containsKey(next.hashCode()) == true){ 147 | originalTasks?.addAll(TaskProcessor.subtasks!![next.hashCode()] ?: mutableListOf()) 148 | tasks?.addAll(TaskProcessor.subtasks!![next.hashCode()] ?: mutableListOf()) 149 | } 150 | } 151 | 152 | /** 153 | * Starts executing the tasks in the control flow sequence. 154 | * @param runAutomaticallyRollback Set to true if you want tasks to automatically rollback on failure. 155 | */ 156 | fun start(runAutomaticallyRollback: Boolean = false) { 157 | workFlowTracker?.started(this@ControlFlow) 158 | this.runAutomaticallyRollback?.set(runAutomaticallyRollback) 159 | runTasks() 160 | } 161 | 162 | private fun runTasks() { 163 | taskJob = safeLauncher { 164 | while (isActive) { 165 | when(tasks?.isNotEmpty()) { 166 | true -> { 167 | tasks?.first()?.let { executeTask(task = it) } 168 | } 169 | else -> { 170 | taskJob?.cancel() 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Starts executing the rollback tasks in the control flow sequence. 179 | */ 180 | 181 | fun startRollback() { 182 | rollbackTaskJob = safeLauncher { 183 | while (isActive) { 184 | if(rollbackTasks?.isNotEmpty() == true){ 185 | rollbackTasks?.last()?.let { executeRollback(task = it) } 186 | }else { 187 | rollbackTaskJob?.cancel() 188 | } 189 | } 190 | } 191 | } 192 | 193 | /** 194 | * Restarts the control flow from the beginning. 195 | */ 196 | fun restart() { 197 | reset() 198 | runAutomaticallyRollback?.get()?.let { start(it) } 199 | } 200 | 201 | /** 202 | * Restarts the rollback process. 203 | */ 204 | fun restartRollback() { 205 | resetRollback() 206 | startRollback() 207 | } 208 | 209 | /** 210 | * Starts executing tasks from a specific task name in the sequence. 211 | * @param taskName The name of the task from which to start the execution. 212 | */ 213 | fun startFrom(taskName: String) { 214 | val index = originalTasks?.indexOfFirst { it.info.name == taskName } 215 | if (index == -1) { 216 | // Task not found, throw an exception or handle it accordingly 217 | return 218 | } 219 | setNewTask(index= index!!) 220 | runAutomaticallyRollback?.let { it.get()?.let { it1 -> start(it1) } } 221 | } 222 | 223 | /** 224 | * Starts executing tasks from a specific task index in the sequence. 225 | * @param taskIndex The index of the task from which to start the execution. 226 | */ 227 | fun startFrom(taskIndex: Int) { 228 | val index = originalTasks?.indexOfFirst { it.info.index == taskIndex } 229 | if (index == -1) { 230 | // Task not found, throw an exception or handle it accordingly 231 | return 232 | } 233 | setNewTask(index= index!!) 234 | runAutomaticallyRollback?.let { it.get()?.let { it1 -> start(it1) } } 235 | } 236 | 237 | private fun setNewTask(index: Int) { 238 | val tasksToExecute = originalTasks?.size?.let { originalTasks?.subList(index, it) } 239 | tasks?.clear() // Clear existing tasks 240 | tasksToExecute?.let { tasks?.addAll(it) } // Set tasks to execute from the specified task onwards 241 | } 242 | 243 | /** 244 | * Initiates the rollback process from a specific task name in the rollback sequence. 245 | * @param taskName The name of the task from which to start the rollback process. 246 | */ 247 | fun startRollbackFrom(taskName: String) { 248 | val index = originalRollbackTasks?.indexOfFirst { it.info.name == taskName } 249 | if (index == -1) { 250 | // Task not found, throw an exception or handle it accordingly 251 | return 252 | } 253 | setNewRollbackTask(index= index!!) 254 | startRollback() 255 | } 256 | 257 | /** 258 | * Initiates the rollback process from a specific task index in the rollback sequence. 259 | * @param taskIndex The index of the task from which to start the rollback process. 260 | */ 261 | fun startRollbackFrom(taskIndex: Int) { 262 | val index = originalRollbackTasks?.indexOfFirst { it.info.index == taskIndex } 263 | if (index == -1) { 264 | // Task not found, throw an exception or handle it accordingly 265 | return 266 | } 267 | setNewRollbackTask(index= index!!) 268 | startRollback() 269 | } 270 | 271 | private fun setNewRollbackTask(index: Int){ 272 | val tasksToExecute = 273 | originalRollbackTasks?.size?.let { originalRollbackTasks?.subList(index, it) } 274 | rollbackTasks?.clear() // Clear existing tasks 275 | tasksToExecute?.let { rollbackTasks?.addAll(it) } // Set tasks to execute from the specified task onwards 276 | 277 | } 278 | 279 | /** 280 | * Associates a callback for the main task execution. 281 | * @param callBack The callback to be associated with the main task execution. 282 | */ 283 | fun useTaskStatusTracker(callBack: TaskStatusTracker){ 284 | this.taskStatusTracker = callBack 285 | } 286 | 287 | /** 288 | * Associates a callback for the rollback task execution. 289 | * @param callBack The callback to be associated with the rollback task execution. 290 | */ 291 | fun useRollbackStatusTracker( callBack: RollbackStatusTracker){ 292 | this.rollbackStatusTracker = callBack 293 | } 294 | 295 | private suspend fun executeTask(task: TaskProcessor) { 296 | val taskFlow = withContext(task.info.runIn) { task.doProcess(param = taskResult) } 297 | taskFlow 298 | .onStart { 299 | if(taskIsCurrentlyInProgress == null){ 300 | workFlowTracker?.taskStatus(controlFlow = this@ControlFlow, taskFlow = TaskFlow().apply { 301 | taskIndex = task.info.index 302 | taskName = task.info.name },state= State.Started) 303 | delay(10L) 304 | workFlowTracker?.taskStatus(controlFlow = this@ControlFlow,taskFlow = TaskFlow().apply { 305 | taskIndex = task.info.index 306 | taskName = task.info.name },state= State.InProgress) 307 | } 308 | } 309 | .retryWhen { cause, attempt -> 310 | val isUseRetryStrategy = task.info.retry?.count != null && 311 | task.info.retry?.count!! > 0 && 312 | !task.info.retry?.causes.isNullOrEmpty() && 313 | task.info.retry?.delay != null && task.info.retry?.delay!! > 0L 314 | 315 | when(isUseRetryStrategy) { 316 | true -> { 317 | if(task.info.retry?.count!! >= attempt+1 && task.info.retry?.causes?.contains(cause::class) == true){ 318 | taskIsCurrentlyInProgress = true 319 | delay(task.info.retry?.delay!!) 320 | return@retryWhen true 321 | }else { 322 | return@retryWhen false 323 | } 324 | } 325 | false -> { 326 | return@retryWhen false 327 | } 328 | } 329 | 330 | } 331 | .catch { 332 | tasks?.remove(element = task) 333 | taskStatusTracker?.failure(controlFlow = this@ControlFlow, info = task.info, errorCause = it) 334 | if(runAutomaticallyRollback?.get() == true) { 335 | if (rollbackTasks?.isNotEmpty() == true) { 336 | startRollback() 337 | }else{ 338 | workFlowTracker?.completed(controlFlow = this@ControlFlow) 339 | } 340 | } else{ 341 | workFlowTracker?.completed(controlFlow = this@ControlFlow) 342 | } 343 | taskIsCurrentlyInProgress = null 344 | taskJob?.cancel() 345 | } 346 | .collect { taskStatus -> 347 | handleTaskStatus(task, taskStatus) 348 | delay(10L) 349 | } 350 | } 351 | 352 | private suspend fun executeRollback(task: RollbackTaskProcessor) { 353 | val rollbackFlow = withContext(task.rollbackInfo.runIn) { task.doRollbackProcess() } 354 | rollbackFlow 355 | .onStart { 356 | if(taskIsCurrentlyInProgress == null){ 357 | workFlowTracker?.taskStatus(controlFlow = this@ControlFlow,taskFlow = TaskFlow().apply { 358 | taskIndex = task.rollbackInfo.index 359 | taskName = task.rollbackInfo.name 360 | isRollback = true 361 | }, state = State.Started) 362 | delay(10L) 363 | workFlowTracker?.taskStatus(controlFlow = this@ControlFlow,taskFlow = TaskFlow().apply { 364 | taskIndex = task.rollbackInfo.index 365 | taskName = task.rollbackInfo.name 366 | isRollback = true 367 | }, state = State.InProgress) 368 | } 369 | } 370 | .retryWhen { cause, attempt -> 371 | val isUseRetryStrategy = task.rollbackInfo.retry?.count != null && 372 | task.rollbackInfo.retry?.count!! > 0 && 373 | !task.rollbackInfo.retry?.causes.isNullOrEmpty() && 374 | task.rollbackInfo.retry?.delay != null && task.rollbackInfo.retry?.delay!! > 0L 375 | 376 | when(isUseRetryStrategy) { 377 | true -> { 378 | if(task.rollbackInfo.retry?.count!! >= attempt+1 && task.rollbackInfo.retry?.causes?.contains(cause::class) == true){ 379 | taskIsCurrentlyInProgress = true 380 | delay(task.rollbackInfo.retry?.delay!!) 381 | return@retryWhen true 382 | }else { 383 | return@retryWhen false 384 | } 385 | } 386 | false -> { 387 | return@retryWhen false 388 | } 389 | } 390 | 391 | } 392 | .catch { 393 | rollbackTasks?.remove(element = task) 394 | rollbackStatusTracker?.failure(controlFlow = this@ControlFlow, info = task.rollbackInfo,errorCause= it) 395 | workFlowTracker?.completed(controlFlow = this@ControlFlow) 396 | rollbackTaskJob?.cancel() 397 | taskIsCurrentlyInProgress = null 398 | } 399 | .collect { rollbackStatus -> 400 | handleRollbackStatus(task, rollbackStatus) 401 | delay(10L) 402 | } 403 | } 404 | 405 | private fun handleTaskStatus(task: TaskProcessor, taskStatus: TaskStatus) { 406 | when (taskStatus) { 407 | is TaskStatus.DoneSuccessfully<*> -> { 408 | tasks?.remove(element = task) 409 | _completedTasks?.add(task) 410 | taskResult = taskStatus.result 411 | taskStatusTracker?.successful(controlFlow = this@ControlFlow, info = task.info, result = taskStatus.result) 412 | if(task is RollbackTaskProcessor) { 413 | originalRollbackTasks?.add(task) 414 | rollbackTasks?.add(task) 415 | } 416 | if(tasks?.isEmpty() == true) { 417 | workFlowTracker?.completed(controlFlow = this@ControlFlow) 418 | taskJob?.cancel() 419 | } 420 | } 421 | } 422 | } 423 | 424 | private fun handleRollbackStatus(task: RollbackTaskProcessor, rollbackStatus: TaskStatus) { 425 | when (rollbackStatus) { 426 | is TaskStatus.DoneSuccessfully<*> -> { 427 | rollbackTasks?.remove(element = task) 428 | _completedRollbackTasks?.add(task) 429 | rollbackStatusTracker?.successful(controlFlow= this@ControlFlow, info = task.rollbackInfo, result = rollbackStatus.result) 430 | if(rollbackTasks?.isEmpty() == true) { 431 | workFlowTracker?.completed(controlFlow = this@ControlFlow) 432 | rollbackTaskJob?.cancel() 433 | } 434 | } 435 | } 436 | 437 | } 438 | 439 | private fun reset(){ 440 | originalRollbackTasks?.clear() 441 | tasks?.clear() 442 | _completedTasks?.clear() 443 | originalTasks?.let { tasks?.addAll(it) } 444 | taskIsCurrentlyInProgress = null 445 | 446 | } 447 | 448 | private fun resetRollback() { 449 | rollbackTasks?.clear() 450 | rollbackTasks = originalRollbackTasks 451 | _completedRollbackTasks?.clear() 452 | taskIsCurrentlyInProgress = null 453 | } 454 | 455 | /** 456 | * Stops and cleans up the resources associated with the task processing system. 457 | * This method sets various internal variables to null, effectively releasing 458 | * the references to the task-related objects, job instances, and other components. 459 | */ 460 | fun stop() { 461 | originalTasks = null 462 | tasks = null 463 | originalRollbackTasks = null 464 | rollbackTasks = null 465 | _completedTasks = null 466 | _completedRollbackTasks = null 467 | runAutomaticallyRollback = null 468 | workFlowTracker = null 469 | taskStatusTracker = null 470 | rollbackStatusTracker = null 471 | taskJob = null 472 | rollbackTaskJob = null 473 | taskResult = null 474 | taskIsCurrentlyInProgress = null 475 | TaskProcessor.subtasks = null 476 | } 477 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/helper/AnyExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.helper 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | fun Boolean.Companion.successMode() = true 7 | 8 | @Keep 9 | fun Boolean.Companion.failureMode() = false -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/helper/Dispatcher.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.helper 2 | 3 | import android.util.Log 4 | import androidx.annotation.Keep 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import io.github.codestarx.models.ConditionData 8 | import io.github.codestarx.models.TransformData 9 | import io.github.codestarx.status.TaskStatus 10 | import kotlinx.coroutines.CoroutineExceptionHandler 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import kotlinx.coroutines.launch 15 | import kotlin.coroutines.resume 16 | import kotlin.coroutines.resumeWithException 17 | import kotlin.coroutines.suspendCoroutine 18 | 19 | typealias Action = (onContinuation: (Any) -> Unit, onFailure: (Throwable) -> Unit) -> Unit 20 | typealias ActionUnit = (onContinuation: () -> Unit,onFailure: (Throwable) -> Unit) -> Unit 21 | const val THROWABLE_DEFAULT_MESSAGE = "You haven't utilized Throwable for This Task. By default, this message is set, but for optimal performance, it's advisable to specifically define the throwable value in relation to the Task." 22 | @Keep 23 | abstract class Dispatcher: ViewModel() { 24 | 25 | private val handler = CoroutineExceptionHandler { _, exception -> 26 | exception.message?.let { Log.e("Dispatcher", it) } 27 | } 28 | 29 | /** 30 | * Safely launches a coroutine with error handling using a provided block. 31 | * Usage: 32 | * Call this method with a suspend function to execute as a coroutine. 33 | * Example: 34 | * ``` 35 | * safeLauncher { mySuspendFunction() } 36 | * ``` 37 | */ 38 | fun safeLauncher(block: suspend CoroutineScope.() -> Unit) { 39 | viewModelScope.launch(handler, block = block) 40 | } 41 | 42 | 43 | /** 44 | * Executes an action and emits TaskStatus based on the result or error. 45 | * No transformation applied to the result. 46 | * Usage: 47 | * Call this method with an action and handle its continuation and failure scenarios. 48 | * Example: 49 | * ``` 50 | * launchAwait(action = { onContinue, onFailure -> myAction(onContinue, onFailure) }) 51 | * ``` 52 | */ 53 | fun launchAwait( 54 | action: Action 55 | ): Flow = flow { 56 | try { 57 | val result = suspendCoroutine { continuation -> 58 | action.invoke({ 59 | continuation.resume(value = TaskStatus.DoneSuccessfully(result = it)) 60 | },{ 61 | continuation.resumeWithException(it) 62 | }) 63 | } 64 | emit(value = result) 65 | }catch (throwable: Throwable){ 66 | throw throwable 67 | } 68 | } 69 | 70 | 71 | /** 72 | * Executes an action and emits TaskStatus based on conditions applied to the result. 73 | * No transformation applied to the result. 74 | * Usage: 75 | * Call this method with an action and conditions to handle different results. 76 | * Example: 77 | * ``` 78 | * launchAwait( 79 | * action = { onContinue, onFailure -> myAction(onContinue, onFailure) }, 80 | * actionCondition = { result -> checkConditions(result) } 81 | * ) 82 | * ``` 83 | */ 84 | @JvmName("launchAwaitCondition") 85 | fun launchAwait( 86 | action: Action, 87 | actionCondition: (Any) -> ConditionData 88 | ): Flow = flow { 89 | try { 90 | val result = suspendCoroutine { continuation -> 91 | action.invoke({ 92 | val result = actionCondition.invoke(it) 93 | when(result.status) { 94 | true -> { 95 | continuation.resume(value = TaskStatus.DoneSuccessfully(result = it)) 96 | } 97 | else -> { 98 | continuation.resumeWithException(result.throwable ?: Throwable(THROWABLE_DEFAULT_MESSAGE) 99 | ) 100 | } 101 | } 102 | },{ 103 | continuation.resumeWithException(it) 104 | }) 105 | } 106 | emit(value = result) 107 | }catch (throwable: Throwable){ 108 | throw throwable 109 | } 110 | } 111 | 112 | /** 113 | * Executes an action and emits TaskStatus based on conditions applied to the result. 114 | * Applies transformation to the result before emitting the status. 115 | * Usage: 116 | * Call this method with an action, a transformation, and conditions to handle different results. 117 | * Example: 118 | * ``` 119 | * launchAwait( 120 | * action = { onContinue, onFailure -> myAction(onContinue, onFailure) }, 121 | * transformer = { result -> transformResult(result) }, 122 | * actionCondition = { result -> checkConditions(result) } 123 | * ) 124 | * ``` 125 | */ 126 | fun launchAwait( 127 | action: Action, 128 | actionCondition: (Any) -> ConditionData, 129 | transformer: (Any) -> TransformData 130 | ): Flow = flow { 131 | try { 132 | val result = suspendCoroutine { continuation -> 133 | action.invoke({ 134 | val result = actionCondition.invoke(it) 135 | when(result.status) { 136 | true -> { 137 | continuation.resume(value = TaskStatus.DoneSuccessfully(result = transformer.invoke(it).data)) 138 | } 139 | else -> { 140 | continuation.resumeWithException(result.throwable ?: Throwable(THROWABLE_DEFAULT_MESSAGE) 141 | ) 142 | } 143 | } 144 | },{ 145 | continuation.resumeWithException(it) 146 | }) 147 | } 148 | emit(value = result) 149 | }catch (throwable: Throwable){ 150 | throw throwable 151 | } 152 | } 153 | 154 | /** 155 | * Executes an action and emits TaskStatus based on the transformed result. 156 | * Applies transformation to the result before emitting the status. 157 | * Usage: 158 | * Call this method with an action and apply a transformation to emit the status. 159 | * Example: 160 | * ``` 161 | * launchAwait( 162 | * action = { onContinue, onFailure -> myAction(onContinue, onFailure) }, 163 | * transformer = { result -> transformResult(result) } 164 | * ) 165 | * ``` 166 | */ 167 | @JvmName("launchAwaitTransformer") 168 | fun launchAwait( 169 | action: Action, 170 | transformer: (Any) -> TransformData 171 | ): Flow = flow { 172 | try { 173 | val result = suspendCoroutine { continuation -> 174 | action.invoke({ 175 | continuation.resume(value = TaskStatus.DoneSuccessfully(result = transformer.invoke(it).data)) 176 | },{ 177 | continuation.resumeWithException(it) 178 | }) 179 | } 180 | emit(value = result) 181 | }catch (throwable: Throwable){ 182 | throw throwable 183 | } 184 | } 185 | 186 | /** 187 | * Executes an action without a result and emits TaskStatus. 188 | * Usage: 189 | * Call this method with an action that doesn't return a result and handle failure scenarios. 190 | * Example: 191 | * ``` 192 | * launchUnitAwait( 193 | * action = { onContinue, onFailure -> myAction(onContinue, onFailure) } 194 | * ) 195 | * ``` 196 | */ 197 | fun launchUnitAwait( 198 | action: ActionUnit 199 | ): Flow = flow { 200 | try { 201 | val result = suspendCoroutine { continuation -> 202 | action.invoke({ 203 | continuation.resume(value = TaskStatus.DoneSuccessfully(result = Unit)) 204 | },{ 205 | continuation.resumeWithException(it) 206 | }) 207 | } 208 | emit(value = result) 209 | }catch (throwable: Throwable){ 210 | throw throwable 211 | } 212 | } 213 | 214 | /** 215 | * Executes a Flow and emits TaskStatus based on conditions applied to the collected result. 216 | * No transformation applied to the result. 217 | * Usage: 218 | * Call this method with a suspend function returning a Flow and handle result conditions. 219 | * Example: 220 | * ``` 221 | * launchFlow( 222 | * action = { myFlowFunction() }, 223 | * actionCondition = { result -> checkConditions(result) } 224 | * ) 225 | * ``` 226 | */ 227 | @JvmName("launchFlowCondition") 228 | fun launchFlow( 229 | action: suspend () -> Flow, 230 | actionCondition: (T) -> ConditionData 231 | ): Flow = flow { 232 | var result: T? = null 233 | try { 234 | action.invoke().collect { value -> 235 | result = value 236 | } 237 | }catch (throwable: Throwable) { 238 | throw throwable 239 | } 240 | finally { 241 | if(result != null){ 242 | val conResult = actionCondition.invoke(result!!) 243 | when(conResult.status){ 244 | true -> { 245 | emit(value = TaskStatus.DoneSuccessfully(result = result)) 246 | } 247 | else -> { 248 | throw conResult.throwable ?: Throwable(THROWABLE_DEFAULT_MESSAGE) 249 | } 250 | } 251 | } 252 | 253 | } 254 | } 255 | 256 | 257 | 258 | /** 259 | * Executes a Flow and emits TaskStatus based on conditions applied to the transformed result. 260 | * Applies transformation to the collected result before emitting the status. 261 | * Usage: 262 | * Call this method with a suspend function returning a Flow, a transformation, and conditions. 263 | * Example: 264 | * ``` 265 | * launchFlow( 266 | * action = { myFlowFunction() }, 267 | * transformer = { result -> transformResult(result) }, 268 | * actionCondition = { result -> checkConditions(result) } 269 | * ) 270 | * ``` 271 | */ 272 | fun launchFlow( 273 | action: suspend () -> Flow, 274 | actionCondition: (T) -> ConditionData, 275 | transformer: (T) -> TransformData, 276 | ): Flow = flow { 277 | var result: T? = null 278 | try { 279 | action.invoke().collect { value -> 280 | result = value 281 | } 282 | }catch (throwable: Throwable) { 283 | throw throwable 284 | } 285 | finally { 286 | if(result != null){ 287 | val conResult = actionCondition.invoke(result!!) 288 | when(conResult.status){ 289 | true -> { 290 | emit(value = TaskStatus.DoneSuccessfully(result = transformer.invoke(result!!).data)) 291 | } 292 | else -> { 293 | throw conResult.throwable ?: 294 | Throwable(THROWABLE_DEFAULT_MESSAGE) 295 | } 296 | } 297 | } 298 | 299 | } 300 | } 301 | 302 | /** 303 | * Executes a Flow and emits TaskStatus based on the transformed result. 304 | * Applies transformation to the collected result before emitting the status. 305 | * Usage: 306 | * Call this method with a suspend function returning a Flow and apply a transformation. 307 | * Example: 308 | * ``` 309 | * launchFlow( 310 | * action = { myFlowFunction() }, 311 | * transformer = { result -> transformResult(result) } 312 | * ) 313 | * ``` 314 | */ 315 | @JvmName("launchFlowTransformer") 316 | fun launchFlow( 317 | action: suspend () -> Flow, 318 | transformer: (T) -> TransformData 319 | ): Flow = flow { 320 | var result: T? = null 321 | try { 322 | action.invoke().collect { value -> 323 | result = value 324 | } 325 | }catch (throwable: Throwable) { 326 | throw throwable 327 | } 328 | finally { 329 | if(result != null){ 330 | emit(value = TaskStatus.DoneSuccessfully(result = transformer.invoke(result!!).data)) 331 | } 332 | 333 | } 334 | } 335 | 336 | /** 337 | * Executes a Flow and emits TaskStatus based on the collected result. 338 | * No conditions or transformations applied to the result. 339 | * Usage: 340 | * Call this method with a suspend function returning a Flow and emit the status based on the result. 341 | * Example: 342 | * ``` 343 | * launchFlow( 344 | * action = { myFlowFunction() } 345 | * ) 346 | * ``` 347 | */ 348 | fun launchFlow( 349 | action: suspend () -> Flow 350 | ): Flow = flow { 351 | var result: T? = null 352 | try { 353 | action.invoke().collect { value -> 354 | result = value 355 | } 356 | }catch (throwable: Throwable) { 357 | throw throwable 358 | } 359 | finally { 360 | if(result != null){ 361 | emit(value = TaskStatus.DoneSuccessfully(result = result)) 362 | } 363 | } 364 | } 365 | 366 | 367 | /** 368 | * Executes an action and emits TaskStatus based on conditions applied to the result. 369 | * Usage: 370 | * Call this method with an action and conditions to handle different results. 371 | * Example: 372 | * ``` 373 | * launch( 374 | * action = { myAction() }, 375 | * actionCondition = { result -> checkConditions(result) } 376 | * ) 377 | * ``` 378 | */ 379 | @JvmName("launchCondition") 380 | fun launch( 381 | action: () -> T, 382 | actionCondition: (T) -> ConditionData 383 | ): Flow = flow { 384 | var result: T? = null 385 | try { 386 | result = action.invoke() 387 | 388 | }catch (throwable: Throwable) { 389 | throw throwable 390 | } 391 | finally { 392 | if(result != null){ 393 | val conResult = actionCondition.invoke(result) 394 | when(conResult.status){ 395 | true -> { 396 | emit(value = TaskStatus.DoneSuccessfully(result = result)) 397 | } 398 | else -> { 399 | throw conResult.throwable ?: Throwable(THROWABLE_DEFAULT_MESSAGE) 400 | } 401 | } 402 | } 403 | } 404 | } 405 | 406 | /** 407 | * Executes an action and emits TaskStatus. 408 | * Usage: 409 | * Call this method with an action and emit the status based on the result or error. 410 | * Example: 411 | * ``` 412 | * launch( 413 | * action = { myAction() } 414 | * ) 415 | * ``` 416 | */ 417 | fun launch( 418 | action: () -> T 419 | ): Flow = flow { 420 | try { 421 | emit(value = TaskStatus.DoneSuccessfully(result = action.invoke())) 422 | }catch (throwable: Throwable) { 423 | throw throwable 424 | } 425 | } 426 | 427 | /** 428 | * Executes an action and emits TaskStatus based on the transformed result. 429 | * Applies transformation to the result before emitting the status. 430 | * Usage: 431 | * Call this method with an action and apply a transformation to emit the status. 432 | * Example: 433 | * ``` 434 | * launch( 435 | * action = { myAction() }, 436 | * transformer = { result -> transformResult(result) } 437 | * ) 438 | * ``` 439 | */ 440 | @JvmName("launchTransformer") 441 | fun launch( 442 | action: () -> T, 443 | transformer: (T) -> TransformData 444 | ): Flow = flow { 445 | try { 446 | emit(value = TaskStatus.DoneSuccessfully(result = transformer.invoke(action.invoke()).data)) 447 | }catch (throwable: Throwable) { 448 | throw throwable 449 | } 450 | } 451 | 452 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/helper/HttpException.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.helper 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | // Custom exception for HTTP errors 7 | class HttpException(val code: Int?, message: String?) : Throwable(message) 8 | -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/interfaces/RollbackStatusTracker.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.interfaces 2 | 3 | import androidx.annotation.Keep 4 | import io.github.codestarx.ControlFlow 5 | import io.github.codestarx.models.RollbackInfo 6 | 7 | @Keep 8 | interface RollbackStatusTracker { 9 | fun successful(controlFlow: ControlFlow, info: RollbackInfo, result: Any?) 10 | fun failure(controlFlow: ControlFlow, info: RollbackInfo, errorCause: Throwable?) 11 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/interfaces/RollbackTaskProcessor.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.interfaces 2 | 3 | import androidx.annotation.Keep 4 | import io.github.codestarx.models.RollbackInfo 5 | import io.github.codestarx.status.TaskStatus 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | @Keep 9 | interface RollbackTaskProcessor : TaskProcessor { 10 | val rollbackInfo: RollbackInfo 11 | suspend fun doRollbackProcess(): Flow 12 | 13 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/interfaces/TaskProcessor.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.interfaces 2 | 3 | import androidx.annotation.Keep 4 | import io.github.codestarx.status.TaskStatus 5 | import io.github.codestarx.models.TaskInfo 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | @Keep 9 | interface TaskProcessor { 10 | 11 | val info: TaskInfo 12 | 13 | suspend fun doProcess(param: Any?): Flow 14 | 15 | companion object { 16 | var subtasks: MutableMap?>? = mutableMapOf() 17 | } 18 | 19 | fun then(subtask: TaskProcessor) { 20 | val key = this@TaskProcessor.hashCode() 21 | if(subtasks?.containsKey(key) == false){ 22 | subtasks!![key] = mutableListOf(subtask) 23 | }else { 24 | val values = subtasks?.getValue(key) 25 | values?.add(subtask) 26 | subtasks?.set(key, values) 27 | } 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/interfaces/TaskStatusTracker.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.interfaces 2 | 3 | import androidx.annotation.Keep 4 | import io.github.codestarx.ControlFlow 5 | import io.github.codestarx.models.TaskInfo 6 | 7 | @Keep 8 | interface TaskStatusTracker { 9 | fun successful(controlFlow: ControlFlow, info: TaskInfo, result: Any?) 10 | fun failure(controlFlow: ControlFlow, info: TaskInfo, errorCause: Throwable?) 11 | 12 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/interfaces/WorkFlowTracker.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.interfaces 2 | 3 | import androidx.annotation.Keep 4 | import io.github.codestarx.ControlFlow 5 | import io.github.codestarx.models.TaskFlow 6 | import io.github.codestarx.status.State 7 | 8 | @Keep 9 | interface WorkFlowTracker { 10 | fun started(controlFlow: ControlFlow) 11 | fun taskStatus(controlFlow: ControlFlow, taskFlow: TaskFlow, state: State) 12 | fun completed(controlFlow: ControlFlow) 13 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/models/ConditionData.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.models 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | data class ConditionData(val status: Boolean?, val throwable: Throwable? = null) -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/models/RetryStrategy.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.models 2 | 3 | import androidx.annotation.Keep 4 | import kotlin.reflect.KClass 5 | 6 | @Keep 7 | class RetryStrategy { 8 | var count: Int? = null 9 | var causes: Set>? = null 10 | var delay: Long? = null 11 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/models/RollbackInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.models 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | class RollbackInfo: TaskInfo() 7 | -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/models/TaskFlow.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.models 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | class TaskFlow { 7 | var taskIndex: Int? = null 8 | var taskName: String? = null 9 | var isRollback: Boolean? = false 10 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/models/TaskInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.models 2 | 3 | import androidx.annotation.Keep 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlin.coroutines.CoroutineContext 6 | @Keep 7 | open class TaskInfo { 8 | var index: Int? = null 9 | var name: String? = null 10 | var retry: RetryStrategy? = null 11 | var runIn: CoroutineContext = Dispatchers.Default 12 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/models/TransformData.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.models 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | data class TransformData(val data: T? = null) 7 | -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/status/State.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.status 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | enum class State { 7 | Started, InProgress 8 | } -------------------------------------------------------------------------------- /ControlFlow/src/main/java/io/github/codestarx/status/TaskStatus.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.status 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | sealed class TaskStatus { 7 | @Keep 8 | data class DoneSuccessfully(val result: T) : TaskStatus() 9 | } 10 | -------------------------------------------------------------------------------- /ControlFlow/src/test/java/io/github/codestarx/ControlFlowTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx 2 | 3 | 4 | import io.github.codestarx.core.BaseUnitTest 5 | import io.github.codestarx.interfaces.RollbackTaskProcessor 6 | import io.github.codestarx.interfaces.TaskProcessor 7 | import io.github.codestarx.interfaces.WorkFlowTracker 8 | import io.github.codestarx.models.TaskInfo 9 | import io.github.codestarx.status.TaskStatus 10 | import io.mockk.coEvery 11 | import io.mockk.coVerifyOrder 12 | import io.mockk.every 13 | import io.mockk.impl.annotations.MockK 14 | import io.mockk.just 15 | import io.mockk.mockk 16 | import io.mockk.runs 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.flow.flowOf 19 | import kotlinx.coroutines.test.TestCoroutineDispatcher 20 | import kotlinx.coroutines.test.resetMain 21 | import kotlinx.coroutines.test.runTest 22 | import kotlinx.coroutines.test.setMain 23 | import org.junit.Assert.assertEquals 24 | import org.junit.Test 25 | 26 | class ControlFlowTest: BaseUnitTest() { 27 | 28 | private lateinit var controlFlow: ControlFlow 29 | 30 | @MockK 31 | private lateinit var workFlowTracker: WorkFlowTracker 32 | @MockK 33 | private lateinit var taskProcessor: TaskProcessor 34 | @MockK 35 | private lateinit var rollbackTaskProcessor: RollbackTaskProcessor 36 | 37 | 38 | private val testDispatcher = TestCoroutineDispatcher() 39 | 40 | override fun onSetUpTest() { 41 | super.onSetUpTest() 42 | // Set the main dispatcher for testing 43 | Dispatchers.setMain(testDispatcher) 44 | controlFlow = ControlFlow(workFlowTracker) 45 | } 46 | 47 | @Test 48 | fun `test adding a task with startWith`() { 49 | controlFlow.startWith(taskProcessor) 50 | 51 | assertEquals(1, (getVariableValue(obj = controlFlow, fieldName = "originalTasks") as MutableList<*>).size) 52 | assertEquals(1, (getVariableValue(obj = controlFlow, fieldName = "tasks") as MutableList<*>).size) 53 | 54 | } 55 | 56 | @Test 57 | fun `test adding a task with then`(){ 58 | controlFlow.startWith(taskProcessor) 59 | controlFlow.then(rollbackTaskProcessor) 60 | 61 | assertEquals(2, (getVariableValue(obj = controlFlow, fieldName = "originalTasks") as MutableList<*>).size) 62 | assertEquals(2, (getVariableValue(obj = controlFlow, fieldName = "tasks") as MutableList<*>).size) 63 | 64 | } 65 | 66 | @Test 67 | fun `test start() method`() = runTest { 68 | val task1 = mockk() 69 | val param = "test_param" 70 | val expectedResult = TaskStatus.DoneSuccessfully("Task1 Done") 71 | 72 | every { task1.info } returns TaskInfo().apply { 73 | index = 0 74 | name = "Task1" 75 | runIn = Dispatchers.IO 76 | } 77 | 78 | coEvery { task1.doProcess(any()) } returns flowOf(expectedResult) 79 | every { workFlowTracker.started(any()) } just runs 80 | every { workFlowTracker.taskStatus(any(),any(),any()) } just runs 81 | every { workFlowTracker.completed(any()) } just runs 82 | 83 | controlFlow.startWith(task1) 84 | controlFlow.start() 85 | 86 | coVerifyOrder{ 87 | workFlowTracker.started(any()) 88 | task1.info 89 | task1.doProcess(any()) 90 | workFlowTracker.taskStatus(any(),any(),any()) 91 | } 92 | 93 | val flow = task1.doProcess(param = param) 94 | 95 | val result = mutableListOf() 96 | flow.collect { 97 | result.add(it) 98 | } 99 | 100 | assert(result.size == 1) 101 | assert(result.first() is TaskStatus.DoneSuccessfully<*>) 102 | assert((result.first() as TaskStatus.DoneSuccessfully<*>).result == expectedResult.result) 103 | 104 | } 105 | 106 | 107 | override fun onStopTest() { 108 | // Reset the main dispatcher after the test completes 109 | Dispatchers.resetMain() 110 | testDispatcher.cleanupTestCoroutines() 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /ControlFlow/src/test/java/io/github/codestarx/DispatcherTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx 2 | 3 | import io.github.codestarx.core.BaseUnitTest 4 | import io.github.codestarx.helper.Dispatcher 5 | import io.github.codestarx.models.ConditionData 6 | import io.github.codestarx.models.TransformData 7 | import io.github.codestarx.status.TaskStatus 8 | import io.mockk.* 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.catch 11 | import kotlinx.coroutines.flow.collect 12 | import kotlinx.coroutines.flow.flowOf 13 | import kotlinx.coroutines.test.runTest 14 | import org.junit.Test 15 | 16 | class DispatcherTest: BaseUnitTest() { 17 | 18 | private val dispatcher = object : Dispatcher() {} 19 | 20 | @Test 21 | fun `test launchAwait with action`() = runTest { 22 | val expectedResult = "Success" 23 | val actionMock = mockk< (onContinuation: (Any) -> Unit, onFailure: (Throwable) -> Unit) -> Unit>() 24 | 25 | coEvery { actionMock.invoke(any(),any()) } answers { 26 | val onContinuation = firstArg<(Any) -> Unit>() 27 | onContinuation.invoke(expectedResult) 28 | } 29 | 30 | val result = mutableListOf() 31 | val flow = dispatcher.launchAwait(actionMock) 32 | flow.collect { 33 | result.add(it) 34 | } 35 | 36 | assert(result.size == 1) 37 | assert(result.first() is TaskStatus.DoneSuccessfully<*>) 38 | assert((result.first() as TaskStatus.DoneSuccessfully<*>).result == expectedResult) 39 | } 40 | 41 | @Test 42 | fun `test launchAwait with action and condition`() = runTest { 43 | val expectedResult = "action result" 44 | val actionMock = mockk< (onContinuation: (Any) -> Unit, onFailure: (Throwable) -> Unit) -> Unit>() 45 | val conditionMock = mockk<(Any) -> ConditionData>() 46 | 47 | coEvery { actionMock.invoke(any(),any()) } answers { 48 | val onContinuation = firstArg<(Any) -> Unit>() 49 | onContinuation.invoke(expectedResult) 50 | } 51 | coEvery { conditionMock.invoke(any()) } returns ConditionData(status = false, throwable = Throwable("an error occurs during execution")) 52 | 53 | val result = mutableListOf() 54 | val flow = dispatcher.launchAwait(actionMock, conditionMock) 55 | flow 56 | .catch { 57 | result.add(it) 58 | } 59 | .collect {} 60 | 61 | assert(result.size == 1) 62 | assert(result.first().message == "an error occurs during execution") 63 | } 64 | 65 | 66 | @Test 67 | fun `test launchAwait with action,condition,transformer`() = runTest { 68 | val expectedResult = "action result" 69 | val actionMock = mockk< (onContinuation: (Any) -> Unit, onFailure: (Throwable) -> Unit) -> Unit>() 70 | val conditionMock = mockk<(Any) -> ConditionData>() 71 | val transformerMock = mockk<(Any) -> TransformData>() 72 | 73 | coEvery { actionMock.invoke(any(),any()) } answers { 74 | val onContinuation = firstArg<(Any) -> Unit>() 75 | onContinuation.invoke(expectedResult) 76 | } 77 | coEvery { conditionMock.invoke(any()) } returns ConditionData(status = true) 78 | coEvery { transformerMock.invoke(any()) } returns TransformData(data = 14000704) 79 | 80 | val result = mutableListOf() 81 | dispatcher.launchAwait(actionMock,conditionMock,transformerMock).collect{ 82 | result.add(it) 83 | } 84 | 85 | assert(result.size == 1) 86 | assert(result.first() is TaskStatus.DoneSuccessfully<*>) 87 | assert((result.first() as TaskStatus.DoneSuccessfully<*>).result == 14000704 ) 88 | 89 | } 90 | 91 | @Test 92 | fun `test launchAwait with action and transformer`() = runTest { 93 | val expectedResult = "action result" 94 | val actionMock = mockk< (onContinuation: (Any) -> Unit, onFailure: (Throwable) -> Unit) -> Unit>() 95 | val transformerMock = mockk<(Any) -> TransformData>() 96 | 97 | coEvery { actionMock.invoke(any(),any()) } answers { 98 | val onContinuation = firstArg<(Any) -> Unit>() 99 | onContinuation.invoke(expectedResult) 100 | } 101 | coEvery { transformerMock.invoke(any()) } returns TransformData(data = 14000704) 102 | 103 | val result = mutableListOf() 104 | dispatcher.launchAwait(actionMock,transformerMock).collect{ 105 | result.add(it) 106 | } 107 | 108 | assert(result.size == 1) 109 | assert(result.first() is TaskStatus.DoneSuccessfully<*>) 110 | assert((result.first() as TaskStatus.DoneSuccessfully<*>).result == 14000704 ) 111 | 112 | } 113 | @Test 114 | fun `test launchUnitAwait`() = runTest { 115 | val actionMock = mockk< (onContinuation: () -> Unit, onFailure: (Throwable) -> Unit) -> Unit>() 116 | 117 | coEvery { actionMock.invoke(any(),any()) } answers { 118 | val onContinuation = firstArg<() -> Unit>() 119 | onContinuation.invoke() 120 | } 121 | 122 | val result = mutableListOf() 123 | dispatcher.launchUnitAwait(actionMock).collect{ 124 | result.add(it) 125 | } 126 | 127 | assert(result.size == 1) 128 | assert(result.first() is TaskStatus.DoneSuccessfully<*>) 129 | assert((result.first() as TaskStatus.DoneSuccessfully<*>).result == Unit ) 130 | 131 | } 132 | 133 | @Test 134 | fun `test launchFlow with action and condition`() = runTest { 135 | val expectedResult = "action result" 136 | val actionMock = mockk<(() -> Flow)>() 137 | val conditionMock = mockk<(Any) -> ConditionData>() 138 | 139 | coEvery { actionMock.invoke() } returns flowOf(expectedResult) 140 | coEvery { conditionMock.invoke(any()) } returns ConditionData(status = true) 141 | 142 | val result = mutableListOf() 143 | val flow = dispatcher.launchFlow(actionMock, conditionMock) 144 | flow.collect { 145 | result.add(it) 146 | } 147 | 148 | assert(result.size == 1) 149 | assert(result.first() is TaskStatus.DoneSuccessfully<*> ) 150 | assert((result.first() as TaskStatus.DoneSuccessfully<*> ).result == "action result") 151 | } 152 | 153 | @Test 154 | fun `test launchFlow with action,condition and transformer`() = runTest { 155 | val expectedResult = "action result" 156 | val actionMock = mockk<(() -> Flow)>() 157 | val conditionMock = mockk<(Any) -> ConditionData>() 158 | val transformerMock = mockk<(Any) -> TransformData>() 159 | 160 | coEvery { actionMock.invoke() } returns flowOf(expectedResult) 161 | coEvery { conditionMock.invoke(any()) } returns ConditionData(status = true) 162 | coEvery { transformerMock.invoke(any()) } returns TransformData(data = "another data") 163 | 164 | val result = mutableListOf() 165 | val flow = dispatcher.launchFlow(actionMock, conditionMock, transformerMock) 166 | flow.collect { 167 | result.add(it) 168 | } 169 | 170 | assert(result.size == 1) 171 | assert(result.first() is TaskStatus.DoneSuccessfully<*> ) 172 | assert((result.first() as TaskStatus.DoneSuccessfully<*> ).result == "another data") 173 | } 174 | 175 | @Test 176 | fun `test launchFlow with action and transformer`() = runTest { 177 | val expectedResult = "action result" 178 | val actionMock = mockk<(() -> Flow)>() 179 | val transformerMock = mockk<(Any) -> TransformData>() 180 | 181 | coEvery { actionMock.invoke() } returns flowOf(expectedResult) 182 | coEvery { transformerMock.invoke(any()) } returns TransformData(data = "another data") 183 | 184 | val result = mutableListOf() 185 | val flow = dispatcher.launchFlow(actionMock, transformerMock) 186 | flow.collect { 187 | result.add(it) 188 | } 189 | 190 | assert(result.size == 1) 191 | assert(result.first() is TaskStatus.DoneSuccessfully<*> ) 192 | assert((result.first() as TaskStatus.DoneSuccessfully<*> ).result == "another data") 193 | } 194 | 195 | @Test 196 | fun `test launchFlow with action`() = runTest { 197 | val expectedResult = "action result" 198 | val actionMock = mockk<(() -> Flow)>() 199 | 200 | coEvery { actionMock.invoke() } returns flowOf(expectedResult) 201 | 202 | val result = mutableListOf() 203 | val flow = dispatcher.launchFlow(actionMock) 204 | flow.collect { 205 | result.add(it) 206 | } 207 | 208 | assert(result.size == 1) 209 | assert(result.first() is TaskStatus.DoneSuccessfully<*> ) 210 | assert((result.first() as TaskStatus.DoneSuccessfully<*> ).result == "action result") 211 | } 212 | 213 | @Test 214 | fun `test launch with action and condition`() = runTest { 215 | val expectedResult = "action result" 216 | val actionMock = mockk<(() -> Any)>() 217 | val conditionMock = mockk<(Any) -> ConditionData>() 218 | 219 | coEvery { actionMock.invoke() } returns expectedResult 220 | coEvery { conditionMock.invoke(any()) } returns ConditionData(status = false,throwable = Throwable("an error occurs during execution")) 221 | 222 | val result = mutableListOf() 223 | val flow = dispatcher.launch(actionMock, conditionMock) 224 | flow 225 | .catch { 226 | result.add(it) 227 | } 228 | .collect() 229 | 230 | assert(result.size == 1) 231 | assert(result.first().message == "an error occurs during execution") 232 | 233 | } 234 | 235 | @Test 236 | fun `test launch with action`() = runTest { 237 | val expectedResult = Throwable("an error occurs during execution") 238 | val actionMock = mockk<(() -> Any)>() 239 | 240 | coEvery { actionMock.invoke() } answers { 241 | throw expectedResult 242 | } 243 | 244 | val result = mutableListOf() 245 | val flow = dispatcher.launch(actionMock) 246 | flow.catch { 247 | result.add(it) 248 | }.collect() 249 | 250 | assert(result.size == 1) 251 | assert(result.first().message == "an error occurs during execution") 252 | 253 | } 254 | 255 | @Test 256 | fun `test launch with action and transformer`() = runTest { 257 | val expectedResult = "action result" 258 | val actionMock = mockk<(() -> Any)>() 259 | val transformerMock = mockk<(Any) -> TransformData>() 260 | 261 | coEvery { actionMock.invoke() } returns expectedResult 262 | coEvery { transformerMock.invoke(any()) } returns TransformData(data = "another data") 263 | 264 | val result = mutableListOf() 265 | val flow = dispatcher.launch(actionMock, transformerMock) 266 | flow.collect { 267 | result.add(it) 268 | } 269 | 270 | assert(result.size == 1) 271 | assert(result.first() is TaskStatus.DoneSuccessfully<*> ) 272 | assert((result.first() as TaskStatus.DoneSuccessfully<*> ).result == "another data") 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /ControlFlow/src/test/java/io/github/codestarx/core/BaseUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.core 2 | 3 | 4 | import io.mockk.MockKAnnotations 5 | import io.mockk.unmockkAll 6 | import org.junit.After 7 | import org.junit.Before 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.BlockJUnit4ClassRunner 10 | import java.lang.reflect.Field 11 | 12 | @RunWith(BlockJUnit4ClassRunner::class) 13 | open class BaseUnitTest { 14 | 15 | open fun onSetUpTest() {} 16 | 17 | open fun onStopTest() {} 18 | 19 | @Before 20 | fun onSetup() { 21 | MockKAnnotations.init(this) 22 | onSetUpTest() 23 | } 24 | 25 | @After 26 | fun onTearDown() { 27 | unmockkAll() 28 | onStopTest() 29 | } 30 | 31 | protected fun getVariableValue(obj: Any, fieldName: String): Any? { 32 | val field: Field = try { 33 | obj.javaClass.getDeclaredField(fieldName) 34 | } catch (e: NoSuchFieldException) { 35 | e.printStackTrace() 36 | return null 37 | } 38 | 39 | field.isAccessible = true 40 | return field.get(obj) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # ControlFlow 6 | 7 | ControlFlow is an Android library that facilitates task sequencing, rollback actions, and error handling. It systematically oversees task execution, offering structured error handling and facilitating rollback processes for efficient management. 8 | 9 | Explore the implementation of the controlflow library through the code samples available in the [ControlFlowDemo](https://github.com/CodeStarX/ControlFlowDemo). repository. 10 | 11 | ## Features 12 | 13 | - **Task Execution Sequencing:** Define and manage a sequence of tasks. 14 | - **Subtasks Execution Sequencing:** Define and manage a sequence of subtasks for each primary task. 15 | - **Rollback Mechanism:** Implement rollback functionalities for tasks that need to revert changes made during their execution. 16 | - **Error Handling:** Handle errors occurring during task execution and initiate rollback processes if required. 17 | - **Automated Data Forwarding:** Each task's output data type is automatically forwarded as the input for the subsequent task by default. 18 | 19 | 20 | ## Installation 21 | 22 | To include ControlFlow in your Android project, add the following dependency to your app's `build.gradle` file: 23 | 24 | ```kotlin 25 | 26 | implementation("io.github.codestarx:control-flow:1.0.0-alpha11") 27 | 28 | repositories { 29 | //.. 30 | //.. 31 | mavenCentral() 32 | } 33 | ``` 34 | 35 | 36 | ### Task Execution with ControlFlow 37 | 38 | 1. **Task Management:** Create instances of tasks inheriting from `Dispatcher` and implementing `TaskProcessor`. 39 | 2. **ControlFlow Class:** Use the `ControlFlow` class to manage task sequences and their execution flow. 40 | 3. **Start Execution:** Begin executing tasks using `start()` method. 41 | 42 | 43 | ## Usage 44 | 45 | ### Task Implementation 46 | 47 | Inherit from Dispatcher: Create tasks by inheriting from the `Dispatcher` class and implementing the `TaskProcessor` properties. For example: 48 | 49 | ```kotlin 50 | class MyTask : Dispatcher(), TaskProcessor { 51 | override val info: TaskInfo 52 | get() = TaskInfo().apply { 53 | index = 0 54 | name = MyTask::class.java.name 55 | runIn = Dispatchers.IO 56 | } 57 | 58 | override suspend fun doProcess(param: Any?): Flow { 59 | // Define the action here 60 | return launchAwait( 61 | action = { 62 | // Perform the action 63 | // ... 64 | }, 65 | transformer = { 66 | // The output generated by this function will serve as the input for the subsequent task 67 | // Return TransformData(data= ...) 68 | }, 69 | actionCondition = { 70 | // Define conditions for continuation or breaking 71 | // Return ConditionData(status = ... , throwable = ... ) 72 | } 73 | ) 74 | } 75 | } 76 | ``` 77 | 78 | 79 | ### Handling Rollback 80 | 81 | Tasks implementing rollback functionality should also override methods from the `RollbackTaskProcessor` interface, specifying rollback actions. 82 | 83 | Example of a task with rollback: 84 | ```kotlin 85 | class MyRollbackTask : Dispatcher(), RollbackTaskProcessor { 86 | override val info: TaskInfo 87 | get() = TaskInfo().apply { 88 | index = 0 89 | name = MyRollbackTask::class.java.name 90 | runIn = Dispatchers.IO 91 | } 92 | 93 | override val rollbackInfo: RollbackInfo 94 | get() = RollbackInfo().apply { 95 | index = 0 96 | name = MyRollbackTask::class.java.name 97 | runIn = Dispatchers.IO 98 | } 99 | 100 | override suspend fun doProcess(param: Any?): Flow { 101 | // Define the action for the task 102 | return launchAwait( 103 | action = { 104 | // Perform the action 105 | // ... 106 | }, 107 | actionCondition = { 108 | // Define conditions for continuation or breaking 109 | // Return ConditionData(status = ... , throwable = ... ) 110 | } 111 | ) 112 | } 113 | 114 | override suspend fun doRollbackProcess(): Flow { 115 | // Define the rollback action here 116 | return launchAwait( 117 | action = { 118 | // Perform the rollback action 119 | // ... 120 | }, 121 | actionCondition = { 122 | // Define conditions for rollback continuation or breaking 123 | // Return ConditionData(status = ... , throwable = ... ) 124 | } 125 | ) 126 | } 127 | } 128 | ``` 129 | 130 | ### Automated Data Forwarding 131 | 132 | Each task's output data type is automatically forwarded as the input for the subsequent task by default. 133 | If you require altering the output data type passed to the next task, utilize the `transformer` method for this purpose. 134 | 135 | Example: 136 | 137 | ```kotlin 138 | class Task : Dispatcher(), TaskProcessor { 139 | override val info: TaskInfo 140 | get() = TaskInfo().apply { 141 | index = 0 142 | name = Task::class.java.name 143 | runIn = Dispatchers.IO 144 | } 145 | 146 | override suspend fun doProcess(param: Any?): Flow { 147 | // Define the action here 148 | return launchAwait( 149 | action = { 150 | // Perform the action 151 | // ... 152 | }, 153 | transformer = { 154 | // The output generated by this function will serve as the input for the subsequent task 155 | // Return TransformData(data= ...) 156 | }, 157 | actionCondition = { 158 | // Define conditions for continuation or breaking 159 | // Return ConditionData(status = ... , throwable = ... ) 160 | } 161 | ) 162 | } 163 | } 164 | 165 | ``` 166 | 167 | ### Attributes Of Each Task 168 | 169 | The attributes of each task are outlined using the `TaskInfo` class. 170 | The ‍‍‍‍`index`, `name` and `runIn` parameters define the task's specifications and execution thread. By utilizing `index` or `name` and the `startFrom` method within the `ControlFlow`, tasks can be rerun as needed. 171 | 172 | Example: 173 | 174 | ```kotlin 175 | class Task : Dispatcher(), TaskProcessor { 176 | get() = TaskInfo().apply { 177 | index = 0 178 | name = Task::class.java.name 179 | runIn = Dispatchers.IO 180 | } 181 | 182 | override suspend fun doProcess(param: Any?): Flow { 183 | ... 184 | } 185 | 186 | ``` 187 | ### Activate The Retry Mechanism For Each Task 188 | 189 | To activate the Retry mechanism for each task, set the `count` to define the number of retries in case of failure. 190 | Additionally, assign specific `causes`, a list of errors, to trigger retries upon encountering these errors. 191 | Adjust the `delay` value to determine the interval between each retry attempt. 192 | 193 | Example: 194 | 195 | ```kotlin 196 | class Task : Dispatcher(), TaskProcessor { 197 | get() = TaskInfo().apply { 198 | index = 0 199 | name = Task::class.java.name 200 | retry = RetryStrategy().apply { 201 | count = 2 202 | causes = setOf(TimeoutException::class,AnotherException::class,...) 203 | delay = 1000L 204 | } 205 | runIn = Dispatchers.IO 206 | } 207 | 208 | override suspend fun doProcess(param: Any?): Flow { 209 | ... 210 | } 211 | 212 | ``` 213 | 214 | ### Attributes Of Each Rollback Task 215 | 216 | The attributes of each task are outlined using the `RollbackInfo` class. 217 | The ‍‍‍‍`index`, `name` and `runIn` parameters define the task's specifications and execution thread. By utilizing `index` or `name` and the `startRollbackFrom` method within the `ControlFlow`, tasks can be rerun as needed. 218 | 219 | Example: 220 | 221 | ```kotlin 222 | class Task : Dispatcher(), RollbackTaskProcessor { 223 | override val info: TaskInfo 224 | get() = TaskInfo().apply { 225 | index = 0 226 | name = Task::class.java.name 227 | runIn = Dispatchers.IO 228 | } 229 | 230 | override val rollbackInfo: RollbackInfo 231 | get() = RollbackInfo().apply { 232 | index = 0 233 | name = Task::class.java.name 234 | runIn = Dispatchers.IO 235 | } 236 | 237 | override suspend fun doProcess(param: Any?): Flow { 238 | ... 239 | } 240 | 241 | override suspend fun doRollbackProcess(): Flow { 242 | ... 243 | } 244 | 245 | ``` 246 | 247 | ### Activate The Retry Mechanism For Each Rollback Task 248 | 249 | To activate the Retry mechanism for each rollback task, set the `count` to define the number of retries in case of failure. 250 | Additionally, assign specific `causes`, a list of errors, to trigger retries upon encountering these errors. 251 | Adjust the `delay` value to determine the interval between each retry attempt. 252 | 253 | Example: 254 | 255 | ```kotlin 256 | class Task : Dispatcher(), RollbackTaskProcessor { 257 | override val info: TaskInfo 258 | get() = ... 259 | 260 | override val rollbackInfo: RollbackInfo 261 | get() = RollbackInfo().apply { 262 | index = 0 263 | name = Task::class.java.name 264 | retry = RetryStrategy().apply { 265 | count = 2 266 | causes = setOf(TimeoutException::class,AnotherException::class,...) 267 | delay = 1000L 268 | } 269 | runIn = Dispatchers.IO 270 | } 271 | 272 | override suspend fun doProcess(param: Any?): Flow { 273 | ... 274 | } 275 | 276 | override suspend fun doRollbackProcess(): Flow { 277 | ... 278 | } 279 | 280 | ``` 281 | 282 | ## ControlFlow Class 283 | `ControlFlow` manages the execution sequence of tasks and potential rollback actions. 284 | It orchestrates the execution, rollback, completion, and error handling of tasks and their rollbacks. 285 | This class offers a structured way to manage a series of tasks and handles their execution flow and potential rollbacks. 286 | 287 | ### Running Control Flow 288 | 289 | Example usage: 290 | 291 | ```kotlin 292 | // Create a ControlFlow instance 293 | val controlFlow = ControlFlow(object : WorkFlowTracker { 294 | // Implement work Flow callback methods 295 | }) 296 | 297 | // Define your tasks 298 | controlFlow.startWith(MyTask()) 299 | controlFlow.then(AnotherTask()) 300 | controlFlow.then(AnotherTask()) 301 | 302 | // Set up TaskStatusTracker if needed 303 | controlFlow.useTaskStatusTracker(object : TaskStatusTracker { 304 | // Implement callback methods 305 | }) 306 | 307 | // Set up RollbackStatusTracker if needed 308 | controlFlow.useRollbackStatusTracker(object : RollbackStatusTracker { 309 | // Implement callback methods 310 | }) 311 | 312 | // Start executing tasks 313 | controlFlow.start() 314 | ``` 315 | 316 | ### Subtasks Execution 317 | To incorporate subtasks for each task, you can define their implementation as outlined below: 318 | 319 | Example usage: 320 | 321 | ```kotlin 322 | // Create a ControlFlow instance 323 | val controlFlow = ControlFlow(object : WorkFlowTracker { 324 | // Implement work Flow callback methods 325 | }) 326 | 327 | // Define your tasks 328 | controlFlow.startWith(MyTask().apply{ 329 | // Define your subtasks 330 | then(subtask= MySubtask()) 331 | then(subtask= AnotherSubtask()) 332 | }) 333 | controlFlow.then(AnotherTask().apply{ 334 | // Define your subtasks 335 | then(subtask= MySubtask()) 336 | then(subtask= AnotherSubtask()) 337 | }) 338 | 339 | // Set up TaskStatusTracker if needed 340 | controlFlow.useTaskStatusTracker(object : TaskStatusTracker { 341 | // Implement callback methods 342 | }) 343 | 344 | // Set up RollbackStatusTracker if needed 345 | controlFlow.useRollbackStatusTracker(object : RollbackStatusTracker { 346 | // Implement callback methods 347 | }) 348 | 349 | // Start executing tasks 350 | controlFlow.start() 351 | ``` 352 | 353 | ### Control-Flow Method Details 354 | ```kotlin 355 | /** 356 | * Add the first task to the control flow sequence. 357 | * @param first task to be added to the control flow sequence. 358 | */ 359 | startWith(first: TaskProcessor) 360 | ``` 361 | ```kotlin 362 | /** 363 | * Adds the next task to the control flow sequence. 364 | * @param next The subsequent task to be added to the control flow sequence. 365 | */ 366 | then(next: TaskProcessor) 367 | ``` 368 | ```kotlin 369 | /** 370 | * Starts executing the tasks in the control flow sequence. 371 | * @param runAutomaticallyRollback Set to true if you want tasks to automatically rollback on failure. 372 | */ 373 | start(runAutomaticallyRollback: Boolean = false) 374 | ``` 375 | ```kotlin 376 | /** 377 | * Starts executing tasks from a specific task name in the sequence. 378 | * @param taskName The name of the task from which to start the execution. 379 | */ 380 | startFrom(taskName: String) 381 | ``` 382 | ```kotlin 383 | /** 384 | * Starts executing tasks from a specific task index in the sequence. 385 | * @param taskIndex The index of the task from which to start the execution. 386 | */ 387 | startFrom(taskIndex: Int) 388 | ``` 389 | ```kotlin 390 | /** 391 | * Restarts the control flow from the beginning. 392 | */ 393 | restart() 394 | ``` 395 | ```kotlin 396 | /** 397 | * Starts executing the rollback tasks in the control flow sequence. 398 | */ 399 | startRollback() 400 | ``` 401 | ```kotlin 402 | /** 403 | * Initiates the rollback process from a specific task name in the rollback sequence. 404 | * @param taskName The name of the task from which to start the rollback process. 405 | */ 406 | startRollbackFrom(taskName: String) 407 | ``` 408 | ```kotlin 409 | /** 410 | * Initiates the rollback process from a specific task index in the rollback sequence. 411 | * @param taskIndex The index of the task from which to start the rollback process. 412 | */ 413 | startRollbackFrom(taskIndex: Int) 414 | ``` 415 | ```kotlin 416 | /** 417 | * Restarts the rollback process. 418 | */ 419 | restartRollback() 420 | ``` 421 | ```kotlin 422 | /** 423 | * Associates a callback for the main task execution. 424 | * @param callBack The callback to be associated with the main task execution. 425 | */ 426 | useTaskStatusTracker(callBack: TaskStatusTracker) 427 | ``` 428 | ```kotlin 429 | /** 430 | * Associates a callback for the rollback task execution. 431 | * @param callBack The callback to be associated with the rollback task execution. 432 | */ 433 | useRollbackStatusTracker( callBack: RollbackStatusTracker) 434 | ``` 435 | ```kotlin 436 | /** 437 | * Stops and cleans up the resources associated with the task processing system. 438 | * This method sets various internal variables to null, effectively releasing 439 | * the references to the task-related objects, job instances, and other components. 440 | */ 441 | fun stop() 442 | ``` 443 | 444 | ## Dispatcher Class 445 | 446 | The `Dispatcher` class serves as a utility to execute asynchronous actions, manage errors, and handle various scenarios using coroutines. This class offers a range of methods to facilitate asynchronous operations and streamline error handling within tasks. 447 | 448 | ### Method Details 449 | 450 | #### `safeLauncher` 451 | 452 | - **Usage**: Executes a provided coroutine block within the `viewModelScope`, managing potential exceptions via a `CoroutineExceptionHandler`. 453 | - **Example**: 454 | ```kotlin 455 | safeLauncher { 456 | // Coroutine block to execute 457 | // ... 458 | } 459 | ``` 460 | 461 | #### `launchAwait` 462 | 463 | - **Usage**: Executes an asynchronous action and emits the result or errors via a Flow, based on specified custom conditions. 464 | - **Example**: 465 | 466 | ```kotlin 467 | launchAwait( 468 | // Action to execute asynchronously with callbacks for continuation and failure 469 | { onContinuation, onFailure -> 470 | // Invoke the action and handle results 471 | // Call onContinuation with the result to continue or onFailure with an error to break 472 | }, 473 | // Condition to check if continuation or breaking is required based on the result 474 | { result -> 475 | // Define conditions for rollback continuation or breaking 476 | // Return ConditionData(status = ... , throwable = ... ) 477 | } 478 | ) 479 | ``` 480 | 481 | #### `launchUnitAwait` 482 | 483 | - **Usage**: Executes an asynchronous action that returns a Unit result and emits the status via a Flow. 484 | - **Example**: 485 | ```kotlin 486 | launchUnitAwait { onContinuation, onFailure -> 487 | // Invoke the action and handle results 488 | // Call onContinuation to indicate successful completion or onFailure with an error 489 | } 490 | ``` 491 | 492 | #### `launchFlow` 493 | 494 | - **Usage**: Executes an asynchronous action returning a Flow, evaluates conditions for success or breaking, and emits the status accordingly. 495 | - **Example**: 496 | ```kotlin 497 | launchFlow( 498 | // Action returning a Flow 499 | { flowAction() }, 500 | // Condition to check if continuation or breaking is required based on the Flow's result 501 | { result -> 502 | // Define conditions for rollback continuation or breaking 503 | // Return ConditionData(status = ... , throwable = ... ) 504 | } 505 | ) 506 | ``` 507 | 508 | #### `launch` 509 | 510 | - **Usage**: Executes a synchronous action, evaluates conditions for success or breaking, and emits the status via a Flow. 511 | - **Example**: 512 | ```kotlin 513 | launch( 514 | // Synchronous action to execute 515 | { syncAction() }, 516 | // Condition to check if continuation or breaking is required based on the action's result 517 | { result -> 518 | // Define conditions for rollback continuation or breaking 519 | // Return ConditionData(status = ... , throwable = ... ) 520 | } 521 | ) 522 | ``` 523 | 524 | ## Documentation 525 | 526 | For detailed information about classes, methods, and functionalities provided by ControlFlow, refer to the inline comments in the source code. 527 | 528 | ## Contributing 529 | 530 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again! 531 | 532 | 1. Fork the Project 533 | 2. Create your Feature Branch (`git checkout -b feature/YourFeatureName`) 534 | 3. Commit your Changes (`git commit -m 'Add some YourFeatureName'`) 535 | 4. Push to the Branch (`git push origin feature/YourFeatureName`) 536 | 5. Open a Pull Request 537 | 538 | ## LICENSE 539 | 540 | The Apache Software License, Version 2.0 541 | 542 | http://www.apache.org/licenses/LICENSE-2.0.txt 543 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "io.github.codestarx.controlflowdevelop" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | applicationId = "io.github.codestarx.controlflowdevelop" 12 | minSdk = 21 13 | targetSdk = 33 14 | versionCode = 1 15 | versionName = "1.0" 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | isMinifyEnabled = true 26 | proguardFiles( 27 | getDefaultProguardFile("proguard-android-optimize.txt"), 28 | "proguard-rules.pro" 29 | ) 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_17 34 | targetCompatibility = JavaVersion.VERSION_17 35 | } 36 | kotlinOptions { 37 | jvmTarget = "17" 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation("androidx.core:core-ktx:1.7.0") 43 | implementation("androidx.appcompat:appcompat:1.6.1") 44 | 45 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/codestarx/controlflowdevelop/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.codestarx.controlflowdevelop 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | 6 | class MainActivity : ComponentActivity() { 7 | override fun onCreate(savedInstanceState: Bundle?) { 8 | super.onCreate(savedInstanceState) 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/ControlFlow/d76e3fd6f00ed713dc5412eb0a07bae4749c174e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ControlFlowDevelop 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |