diff --git a/README.md b/README.md index 9920898..07869eb 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ result of this call. ```kotlin class LoginUseCase @Inject constructor( private val apiManager: ApiManager // Retrofit Service -) : SinglerUseCase() { +) : SingleUseCase() { override fun prepare(args: LoginData): Single { return apiManager.getUser(args) @@ -455,9 +455,9 @@ perceived on the same domain level as stores. Thanks to use cases we can easily manipulate and combine this kind of data on background threads. ```kotlin -class GetUserFullNameObservabler @Inject constructor( +class GetUserFullNameObservableUseCase @Inject constructor( private val userStore: UserStore -) : ObservablerUseCase() { +) : ObservableUseCase() { override fun prepare(): Observable { return userStore.getUser() diff --git a/bindingadapters/build.gradle.kts b/bindingadapters/build.gradle.kts index c2cf273..d075a54 100644 --- a/bindingadapters/build.gradle.kts +++ b/bindingadapters/build.gradle.kts @@ -22,6 +22,7 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += "-Xcontext-receivers" } dataBinding { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 5fa0dcd..e16423f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -32,6 +32,7 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += "-Xcontext-receivers" } } diff --git a/cr-usecases/build.gradle.kts b/cr-usecases/build.gradle.kts index 5398b44..e8e0b47 100644 --- a/cr-usecases/build.gradle.kts +++ b/cr-usecases/build.gradle.kts @@ -25,6 +25,7 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += "-Xcontext-receivers" } } diff --git a/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/CoroutineScopeOwner.kt b/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/CoroutineScopeOwner.kt index 14fd65f..5feca55 100644 --- a/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/CoroutineScopeOwner.kt +++ b/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/CoroutineScopeOwner.kt @@ -4,15 +4,7 @@ import app.futured.arkitekt.core.error.UseCaseErrorHandler import app.futured.arkitekt.crusecases.utils.rootCause import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch /** @@ -33,215 +25,6 @@ interface CoroutineScopeOwner { */ fun getWorkerDispatcher() = Dispatchers.IO - /** - * Asynchronously executes use case and saves it's Deferred. By default all previous - * pending executions are canceled, this can be changed by the [config]. - * This version is used for use cases without initial arguments. - * - * @param config [UseCaseConfig] used to process results of internal - * Coroutine and to set configuration options. - */ - fun UseCase.execute(config: UseCaseConfig.Builder.() -> Unit) = - execute(Unit, config) - - /** - * Asynchronously executes use case and saves it's Deferred. By default all previous - * pending executions are canceled, this can be changed by the [config]. - * This version gets initial arguments by [args]. - * - * In case that an error is thrown during the execution of [UseCase] then [UseCaseErrorHandler.globalOnErrorLogger] - * is called with the error as an argument. - * - * @param args Arguments used for initial use case initialization. - * @param config [UseCaseConfig] used to process results of internal - * Coroutine and to set configuration options. - */ - fun UseCase.execute( - args: ARGS, - config: UseCaseConfig.Builder.() -> Unit - ) { - val useCaseConfig = UseCaseConfig.Builder().run { - config.invoke(this) - return@run build() - } - if (useCaseConfig.disposePrevious) { - deferred?.cancel() - } - - useCaseConfig.onStart() - deferred = coroutineScope.async(getWorkerDispatcher(), CoroutineStart.LAZY) { - build(args) - }.also { - coroutineScope.launch(Dispatchers.Main) { - try { - useCaseConfig.onSuccess(it.await()) - } catch (cancellation: CancellationException) { - // do nothing - this is normal way of suspend function interruption - } catch (error: Throwable) { - UseCaseErrorHandler.globalOnErrorLogger(error) - useCaseConfig.onError.invoke(error) - } - } - } - } - - /** - * Synchronously executes use case and saves it's Deferred. By default all previous - * pending executions are canceled, this can be changed by the [cancelPrevious]. - * This version is used for use cases without initial arguments. - * - * @return [Result] that encapsulates either a successful result with [Success] or a failed result with [Error] - */ - suspend fun UseCase.execute(cancelPrevious: Boolean = true) = execute(Unit, cancelPrevious) - - /** - * Synchronously executes use case and saves it's Deferred. By default all previous - * pending executions are canceled, this can be changed by the [cancelPrevious]. - * This version gets initial arguments by [args]. - * - * [UseCaseErrorHandler.globalOnErrorLogger] is not used in this version of the execute method since it is - * recommended to call all execute methods with [Result] return type from [launchWithHandler] method where - * [UseCaseErrorHandler.globalOnErrorLogger] is used. - * - * @param args Arguments used for initial use case initialization. - * @return [Result] that encapsulates either a successful result with [Success] or a failed result with [Error] - */ - suspend fun UseCase.execute(args: ARGS, cancelPrevious: Boolean = true): Result { - if (cancelPrevious) { - deferred?.cancel() - } - return try { - val newDeferred = coroutineScope.async(getWorkerDispatcher(), CoroutineStart.LAZY) { - build(args) - }.also { deferred = it } - Success(newDeferred.await()) - } catch (exception: CancellationException) { - throw exception - } catch (exception: Throwable) { - Error(exception) - } - } - - /** - * Holds references to lambdas and some basic configuration - * used to process results of Coroutine use case. - * Use [UseCaseConfig.Builder] to construct this object. - */ - class UseCaseConfig private constructor( - val onStart: () -> Unit, - val onSuccess: (T) -> Unit, - val onError: (Throwable) -> Unit, - val disposePrevious: Boolean - ) { - /** - * Constructs references to lambdas and some basic configuration - * used to process results of Coroutine use case. - */ - class Builder { - private var onStart: (() -> Unit)? = null - private var onSuccess: ((T) -> Unit)? = null - private var onError: ((Throwable) -> Unit)? = null - private var disposePrevious = true - - /** - * Set lambda that is called right before - * the internal Coroutine is created - * @param onStart Lambda called right before Coroutine is - * created - */ - fun onStart(onStart: () -> Unit) { - this.onStart = onStart - } - - /** - * Set lambda that is called when internal Coroutine - * finished without exceptions - * @param onSuccess Lambda called when Coroutine finished - */ - fun onSuccess(onSuccess: (T) -> Unit) { - this.onSuccess = onSuccess - } - - /** - * Set lambda that is called when exception on - * internal Coroutine occurs - * @param onError Lambda called when exception occurs - */ - fun onError(onError: (Throwable) -> Unit) { - this.onError = onError - } - - /** - * Set whether currently active Job of internal Coroutine - * should be canceled when execute is called repeatedly. - * Default value is true. - * @param disposePrevious True if active Job of internal - * Coroutine should be canceled. Default value is true. - */ - fun disposePrevious(disposePrevious: Boolean) { - this.disposePrevious = disposePrevious - } - - fun build(): UseCaseConfig { - return UseCaseConfig( - onStart ?: { }, - onSuccess ?: { }, - onError ?: { throw it }, - disposePrevious - ) - } - } - } - - fun FlowUseCase.execute(config: FlowUseCaseConfig.Builder.() -> Unit) = - execute(Unit, config) - - /** - * Asynchronously executes use case and consumes data from flow on UI thread. - * By default all previous pending executions are canceled, this can be changed - * by [config]. When suspend function in use case finishes, onComplete is called - * on UI thread. This version is gets initial arguments by [args]. - * - * In case that an error is thrown during the execution of [FlowUseCase] then - * [UseCaseErrorHandler.globalOnErrorLogger] is called with the error as an argument. - * - * @param args Arguments used for initial use case initialization. - * @param config [FlowUseCaseConfig] used to process results of internal - * Flow and to set configuration options. - **/ - fun FlowUseCase.execute( - args: ARGS, - config: FlowUseCaseConfig.Builder.() -> Unit - ) { - val flowUseCaseConfig = FlowUseCaseConfig.Builder().run { - config.invoke(this) - return@run build() - } - - if (flowUseCaseConfig.disposePrevious) { - job?.cancel() - } - - job = build(args) - .flowOn(getWorkerDispatcher()) - .onStart { flowUseCaseConfig.onStart() } - .onEach { flowUseCaseConfig.onNext(it) } - .onCompletion { error -> - when { - error is CancellationException -> { - // ignore this exception - } - error != null -> { - UseCaseErrorHandler.globalOnErrorLogger(error) - flowUseCaseConfig.onError(error) - } - else -> flowUseCaseConfig.onComplete() - } - } - .catch { /* handled in onCompletion */ } - .launchIn(coroutineScope) - } - /** * Launch suspend [block] in [coroutineScope]. Encapsulates this call with try catch block and when an exception is thrown * then it is logged in [UseCaseErrorHandler.globalOnErrorLogger] and handled by [defaultErrorHandler]. @@ -273,85 +56,4 @@ interface CoroutineScopeOwner { fun defaultErrorHandler(exception: Throwable) { throw exception } - - /** - * Holds references to lambdas and some basic configuration - * used to process results of Flow use case. - * Use [FlowUseCaseConfig.Builder] to construct this object. - */ - class FlowUseCaseConfig private constructor( - val onStart: () -> Unit, - val onNext: (T) -> Unit, - val onError: (Throwable) -> Unit, - val onComplete: () -> Unit, - val disposePrevious: Boolean - ) { - /** - * Constructs references to lambdas and some basic configuration - * used to process results of Flow use case. - */ - class Builder { - private var onStart: (() -> Unit)? = null - private var onNext: ((T) -> Unit)? = null - private var onError: ((Throwable) -> Unit)? = null - private var onComplete: (() -> Unit)? = null - private var disposePrevious = true - - /** - * Set lambda that is called right before - * internal Job of Flow is launched. - * @param onStart Lambda called right before Flow Job is launched. - */ - fun onStart(onStart: () -> Unit) { - this.onStart = onStart - } - - /** - * Set lambda that is called when internal Flow emits new value - * @param onNext Lambda called for every new emitted value - */ - fun onNext(onNext: (T) -> Unit) { - this.onNext = onNext - } - - /** - * Set lambda that is called when some exception on - * internal Flow occurs - * @param onError Lambda called when exception occurs - */ - fun onError(onError: (Throwable) -> Unit) { - this.onError = onError - } - - /** - * Set lambda that is called when internal Flow is completed - * without errors - * @param onComplete Lambda called when Flow is completed - * without errors - */ - fun onComplete(onComplete: () -> Unit) { - this.onComplete = onComplete - } - - /** - * Set whether currently running Job of internal Flow - * should be canceled when execute is called repeatedly. - * @param disposePrevious True if currently running - * Job of internal Flow should be canceled - */ - fun disposePrevious(disposePrevious: Boolean) { - this.disposePrevious = disposePrevious - } - - fun build(): FlowUseCaseConfig { - return FlowUseCaseConfig( - onStart ?: { }, - onNext ?: { }, - onError ?: { throw it }, - onComplete ?: { }, - disposePrevious - ) - } - } - } } diff --git a/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/FlowUseCase.kt b/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/FlowUseCase.kt index 39ffa05..8aeff89 100644 --- a/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/FlowUseCase.kt +++ b/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/FlowUseCase.kt @@ -1,7 +1,15 @@ package app.futured.arkitekt.crusecases +import app.futured.arkitekt.core.error.UseCaseErrorHandler +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart /** * Base [Flow] use case meant to use in [CoroutineScopeOwner] implementations @@ -18,4 +26,135 @@ abstract class FlowUseCase { * @param args initial use case arguments */ abstract fun build(args: ARGS): Flow + + context(CoroutineScopeOwner) + fun execute(config: FlowUseCaseConfig.Builder.() -> Unit) = execute(args = Unit as ARGS,config = config) + + /** + * Asynchronously executes use case and consumes data from flow on UI thread. + * By default all previous pending executions are canceled, this can be changed + * by [config]. When suspend function in use case finishes, onComplete is called + * on UI thread. This version is gets initial arguments by [args]. + * + * In case that an error is thrown during the execution of [FlowUseCase] then + * [UseCaseErrorHandler.globalOnErrorLogger] is called with the error as an argument. + * + * @param args Arguments used for initial use case initialization. + * @param config [FlowUseCaseConfig] used to process results of internal + * Flow and to set configuration options. + **/ + context(CoroutineScopeOwner) + fun execute( + args: ARGS, + config: FlowUseCaseConfig.Builder.() -> Unit + ) { + val flowUseCaseConfig = FlowUseCaseConfig.Builder().run { + config.invoke(this) + return@run build() + } + + if (flowUseCaseConfig.disposePrevious) { + job?.cancel() + } + + job = build(args) + .flowOn(getWorkerDispatcher()) + .onStart { flowUseCaseConfig.onStart() } + .onEach { flowUseCaseConfig.onNext(it) } + .onCompletion { error -> + when { + error is CancellationException -> { + // ignore this exception + } + error != null -> { + UseCaseErrorHandler.globalOnErrorLogger(error) + flowUseCaseConfig.onError(error) + } + else -> flowUseCaseConfig.onComplete() + } + } + .catch { /* handled in onCompletion */ } + .launchIn(coroutineScope) + } + + /** + * Holds references to lambdas and some basic configuration + * used to process results of Flow use case. + * Use [FlowUseCaseConfig.Builder] to construct this object. + */ + class FlowUseCaseConfig private constructor( + val onStart: () -> Unit, + val onNext: (T) -> Unit, + val onError: (Throwable) -> Unit, + val onComplete: () -> Unit, + val disposePrevious: Boolean + ) { + /** + * Constructs references to lambdas and some basic configuration + * used to process results of Flow use case. + */ + class Builder { + private var onStart: (() -> Unit)? = null + private var onNext: ((T) -> Unit)? = null + private var onError: ((Throwable) -> Unit)? = null + private var onComplete: (() -> Unit)? = null + private var disposePrevious = true + + /** + * Set lambda that is called right before + * internal Job of Flow is launched. + * @param onStart Lambda called right before Flow Job is launched. + */ + fun onStart(onStart: () -> Unit) { + this.onStart = onStart + } + + /** + * Set lambda that is called when internal Flow emits new value + * @param onNext Lambda called for every new emitted value + */ + fun onNext(onNext: (T) -> Unit) { + this.onNext = onNext + } + + /** + * Set lambda that is called when some exception on + * internal Flow occurs + * @param onError Lambda called when exception occurs + */ + fun onError(onError: (Throwable) -> Unit) { + this.onError = onError + } + + /** + * Set lambda that is called when internal Flow is completed + * without errors + * @param onComplete Lambda called when Flow is completed + * without errors + */ + fun onComplete(onComplete: () -> Unit) { + this.onComplete = onComplete + } + + /** + * Set whether currently running Job of internal Flow + * should be canceled when execute is called repeatedly. + * @param disposePrevious True if currently running + * Job of internal Flow should be canceled + */ + fun disposePrevious(disposePrevious: Boolean) { + this.disposePrevious = disposePrevious + } + + fun build(): FlowUseCaseConfig { + return FlowUseCaseConfig( + onStart ?: { }, + onNext ?: { }, + onError ?: { throw it }, + onComplete ?: { }, + disposePrevious + ) + } + } + } } diff --git a/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/UseCase.kt b/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/UseCase.kt index 93c356c..7680940 100644 --- a/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/UseCase.kt +++ b/cr-usecases/src/main/java/app/futured/arkitekt/crusecases/UseCase.kt @@ -1,6 +1,12 @@ package app.futured.arkitekt.crusecases +import app.futured.arkitekt.core.error.UseCaseErrorHandler +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch /** * Base Coroutine use case meant to use in [CoroutineScopeOwner] implementations @@ -15,4 +21,167 @@ abstract class UseCase { * Suspend function which should contain business logic */ abstract suspend fun build(args: ARGS): T + + /** + * Asynchronously executes use case and saves it's Deferred. By default all previous + * pending executions are canceled, this can be changed by the [config]. + * This version is used for use cases without initial arguments. + * + * @param config [UseCaseConfig] used to process results of internal + * Coroutine and to set configuration options. + */ + context(CoroutineScopeOwner) + fun execute(config: UseCaseConfig.Builder.() -> Unit) = execute(args = Unit as ARGS, config = config) + + /** + * Asynchronously executes use case and saves it's Deferred. By default all previous + * pending executions are canceled, this can be changed by the [config]. + * This version gets initial arguments by [args]. + * + * In case that an error is thrown during the execution of [UseCase] then [UseCaseErrorHandler.globalOnErrorLogger] + * is called with the error as an argument. + * + * @param args Arguments used for initial use case initialization. + * @param config [UseCaseConfig] used to process results of internal + * Coroutine and to set configuration options. + */ + context(CoroutineScopeOwner) + fun execute( + args: ARGS, + config: UseCaseConfig.Builder.() -> Unit + ) { + val useCaseConfig = UseCaseConfig.Builder().run { + config.invoke(this) + return@run build() + } + if (useCaseConfig.disposePrevious) { + deferred?.cancel() + } + + useCaseConfig.onStart() + deferred = coroutineScope.async(getWorkerDispatcher(), CoroutineStart.LAZY) { + build(args) + }.also { + coroutineScope.launch(Dispatchers.Main) { + try { + useCaseConfig.onSuccess(it.await()) + } catch (cancellation: CancellationException) { + // do nothing - this is normal way of suspend function interruption + } catch (error: Throwable) { + UseCaseErrorHandler.globalOnErrorLogger(error) + useCaseConfig.onError.invoke(error) + } + } + } + } + + /** + * Synchronously executes use case and saves it's Deferred. By default all previous + * pending executions are canceled, this can be changed by the [cancelPrevious]. + * This version is used for use cases without initial arguments. + * + * @return [Result] that encapsulates either a successful result with [Success] or a failed result with [Error] + */ + context(CoroutineScopeOwner) + suspend fun execute(cancelPrevious: Boolean = true) = execute(args = Unit as ARGS,cancelPrevious = cancelPrevious) + + /** + * Synchronously executes use case and saves it's Deferred. By default all previous + * pending executions are canceled, this can be changed by the [cancelPrevious]. + * This version gets initial arguments by [args]. + * + * [UseCaseErrorHandler.globalOnErrorLogger] is not used in this version of the execute method since it is + * recommended to call all execute methods with [Result] return type from [launchWithHandler] method where + * [UseCaseErrorHandler.globalOnErrorLogger] is used. + * + * @param args Arguments used for initial use case initialization. + * @return [Result] that encapsulates either a successful result with [Success] or a failed result with [Error] + */ + context(CoroutineScopeOwner) + suspend fun execute(args: ARGS, cancelPrevious: Boolean = true): Result { + if (cancelPrevious) { + deferred?.cancel() + } + return try { + val newDeferred = coroutineScope.async(getWorkerDispatcher(), CoroutineStart.LAZY) { + build(args) + }.also { deferred = it } + Success(newDeferred.await()) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Throwable) { + Error(exception) + } + } + + /** + * Holds references to lambdas and some basic configuration + * used to process results of Coroutine use case. + * Use [UseCaseConfig.Builder] to construct this object. + */ + class UseCaseConfig private constructor( + val onStart: () -> Unit, + val onSuccess: (T) -> Unit, + val onError: (Throwable) -> Unit, + val disposePrevious: Boolean + ) { + /** + * Constructs references to lambdas and some basic configuration + * used to process results of Coroutine use case. + */ + class Builder { + private var onStart: (() -> Unit)? = null + private var onSuccess: ((T) -> Unit)? = null + private var onError: ((Throwable) -> Unit)? = null + private var disposePrevious = true + + /** + * Set lambda that is called right before + * the internal Coroutine is created + * @param onStart Lambda called right before Coroutine is + * created + */ + fun onStart(onStart: () -> Unit) { + this.onStart = onStart + } + + /** + * Set lambda that is called when internal Coroutine + * finished without exceptions + * @param onSuccess Lambda called when Coroutine finished + */ + fun onSuccess(onSuccess: (T) -> Unit) { + this.onSuccess = onSuccess + } + + /** + * Set lambda that is called when exception on + * internal Coroutine occurs + * @param onError Lambda called when exception occurs + */ + fun onError(onError: (Throwable) -> Unit) { + this.onError = onError + } + + /** + * Set whether currently active Job of internal Coroutine + * should be canceled when execute is called repeatedly. + * Default value is true. + * @param disposePrevious True if active Job of internal + * Coroutine should be canceled. Default value is true. + */ + fun disposePrevious(disposePrevious: Boolean) { + this.disposePrevious = disposePrevious + } + + fun build(): UseCaseConfig { + return UseCaseConfig( + onStart ?: { }, + onSuccess ?: { }, + onError ?: { throw it }, + disposePrevious + ) + } + } + } } diff --git a/dagger/build.gradle.kts b/dagger/build.gradle.kts index 9712e9b..a6afa12 100644 --- a/dagger/build.gradle.kts +++ b/dagger/build.gradle.kts @@ -22,6 +22,7 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += "-Xcontext-receivers" } } diff --git a/example-hilt/build.gradle.kts b/example-hilt/build.gradle.kts index 17bc3db..f565685 100644 --- a/example-hilt/build.gradle.kts +++ b/example-hilt/build.gradle.kts @@ -29,6 +29,7 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += "-Xcontext-receivers" } } diff --git a/example-minimal/build.gradle.kts b/example-minimal/build.gradle.kts index d7f61f4..a8c6de0 100644 --- a/example-minimal/build.gradle.kts +++ b/example-minimal/build.gradle.kts @@ -26,6 +26,7 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += "-Xcontext-receivers" } } diff --git a/example/build.gradle.kts b/example/build.gradle.kts index b389758..84c8053 100644 --- a/example/build.gradle.kts +++ b/example/build.gradle.kts @@ -40,6 +40,7 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += "-Xcontext-receivers" } configurations.all { diff --git a/rx-usecases/build.gradle.kts b/rx-usecases/build.gradle.kts index 0389801..cfac2c7 100644 --- a/rx-usecases/build.gradle.kts +++ b/rx-usecases/build.gradle.kts @@ -25,6 +25,7 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += "-Xcontext-receivers" } }