Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-04 13:07:18 +03:00
parent bc00198797
commit ddb8de4964
20 changed files with 447 additions and 84 deletions

View file

@ -0,0 +1,96 @@
package com.tangem.domain.core.lce
import arrow.core.identity
import com.tangem.domain.core.utils.flatMap
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
/**
* A sealed class representing the three states of a data load operation: Loading, Content, and Error.
*
* @param E The type of the error object.
* @param C The type of the content object.
*/
sealed class Lce<out E : Any, out C : Any> {
/**
* Represents the loading state, which may contain partial content.
*
* @param partialContent The partial content that has been loaded so far, if any.
*/
data class Loading<C : Any>(val partialContent: C?) : Lce<Nothing, C>()
/**
* Represents the content state, which contains the loaded content.
*
* @param content The loaded content.
*/
data class Content<C : Any>(val content: C) : Lce<Nothing, C>()
/**
* Represents the error state, which contains an error object.
*
* @param error The error that occurred during loading.
*/
data class Error<E : Any>(val error: E) : Lce<E, Nothing>()
/**
* Applies the given functions to the content, error, or partial content of this Lce, depending on its state.
*
* @param ifLoading The function to apply if this is a [Loading] state.
* @param ifContent The function to apply if this is a [Content] state.
* @param ifError The function to apply if this is an [Error] state.
* @return The result of applying the corresponding function.
*/
inline fun <T> fold(
ifLoading: (partialContent: C?) -> T,
ifContent: (content: C) -> T,
ifError: (error: E) -> T,
): T = when (this) {
is Loading -> ifLoading(partialContent)
is Error -> ifError(error)
is Content -> ifContent(content)
}
/**
* Transforms the content of this [Lce] by applying the given function.
* If this is a [Content] state, the function is applied to the [Content.content].
* If this is a [Loading] state and partialContent is present,
* the function is applied to the [Loading.partialContent].
*
* @param ifContent The function to apply to the content or partial content.
* @return A new [Lce] instance containing the result of applying the function.
*/
inline fun <T : Any> map(ifContent: (C) -> T): Lce<E, T> = flatMap { content, isLoading ->
if (isLoading) {
lceLoading(ifContent(content))
} else {
ifContent(content).lceContent()
}
}
/**
* Transforms the error of this [Lce] by applying the given function, if this is an [Error] state.
*
* @param ifError The function to apply to the error.
* @return A new [Lce] with the transformed error, or this [Lce] unchanged if it is not an [Error] state.
*/
inline fun <T : Any> mapError(ifError: (E) -> T): Lce<T, C> = when (this) {
is Loading -> this
is Content -> this
is Error -> ifError(error).lceError()
}
/**
* Returns the content of this [Lce] if it's a [Lce.Content] or partial content if it's a [Lce.Loading],
* `null` if it's a [Lce.Error].
*
* @return The content of this [Lce] or `null`.
*/
fun getOrNull(): C? = fold(
ifLoading = ::identity,
ifContent = ::identity,
ifError = { null },
)
}

View file

@ -0,0 +1,99 @@
package com.tangem.domain.core.lce
import arrow.core.raise.Raise
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlin.experimental.ExperimentalTypeInference
/**
* A [Flow] of [Lce]
*
* @param E The type of the error object.
* @param C The type of the content object.
* */
typealias LceFlow<E, C> = Flow<Lce<E, C>>
/**
* A class that wraps a [LceRaise] instance for [Lce] type within a [ProducerScope].
* It provides methods to handle [Lce] instances and raise errors within a [Flow].
*
* @property raise The [LceRaise] instance that this class wraps.
* @property scope The [ProducerScope] that this class operates within.
* @property ifLoading The function to call if a loading state is raised.
*/
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
private val raise: LceRaise<E>,
private val scope: ProducerScope<Lce<E, C>>,
private val ifLoading: LceRaise<E>.(C) -> Lce<E, C>,
) : Raise<Lce<E, Nothing>> by raise,
CoroutineScope by scope {
/**
* Raises an [Lce] instance within the [ProducerScope].
* It closes the [ProducerScope] after raise.
*
* @param r The [Lce] instance to raise.
*/
override fun raise(r: Lce<E, Nothing>): Nothing {
scope.launch(NonCancellable) {
scope.send(r)
scope.close()
}
raise.raise(r)
}
/**
* Sends a content value within the [ProducerScope].
* If the content is still loading, it calls [ifLoading] lambda to retrieve a state.
* Otherwise, it wraps the content in a [Lce.Content] state.
*
* @param content The content value to send.
* @param isStillLoading A flag indicating whether the content is still loading.
*/
suspend fun send(content: C, isStillLoading: Boolean = false) {
val value = if (isStillLoading) {
ifLoading(raise, content)
} else {
content.lceContent()
}
scope.send(value)
}
}
/**
* Creates a [LceFlow] by executing the given [block] within a [LceFlowScope] context.
*
* Flow starts with a [Lce.Loading] state.
*
* @param ifLoading The function to call if the [block] raises a [Lce.Loading] state.
* By default, it creates a new [Lce.Loading] state with the value returned by the [block].
* @param block The block to execute within a [LceFlowScope] context.
* @return A [LceFlow] representing the result of the [block].
*/
@OptIn(ExperimentalTypeInference::class)
fun <E : Any, C : Any> lceFlow(
ifLoading: LceRaise<E>.(C) -> Lce<E, C> = { lceLoading(partialContent = it) },
@BuilderInference block: suspend LceFlowScope<E, C>.() -> Unit,
): LceFlow<E, C> {
return channelFlow {
trySend(lceLoading())
lce {
val scope = LceFlowScope(
raise = this@lce,
scope = this@channelFlow,
ifLoading = ifLoading,
)
block(scope)
}
}
}

View file

@ -0,0 +1,86 @@
package com.tangem.domain.core.lce
import arrow.atomic.Atomic
import arrow.core.raise.Raise
import arrow.core.raise.recover
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import kotlin.experimental.ExperimentalTypeInference
/**
* A class that wraps a [Raise] instance for [Lce] type.
* It provides methods to handle [Lce] instances and raise errors.
*
* @property raise The [Raise] instance that this class wraps.
* @property isLoading An [Atomic] boolean flag indicating whether a loading operation is in progress.
*/
class LceRaise<E : Any> @PublishedApi internal constructor(
private val raise: Raise<Lce<E, Nothing>>,
) : Raise<Lce<E, Nothing>> by raise {
val isLoading: Atomic<Boolean> = Atomic(false)
/**
* Binds the content of this [Lce] instance and handles its state.
* If this is a [Lce.Loading] state, sets the [isLoading] flag to true and calls the [ifLoading] function.
* If this is a [Lce.Content] state, returns the content.
* If this is a [Lce.Error] state, raises the error.
*
* @param ifLoading The function to call if this is a [Lce.Loading] state.
* By default, it raises a new [Lce.Loading] state.
* @return The content of this [Lce] instance.
*/
fun <C : Any> Lce<E, C>.bind(ifLoading: (partialContent: C?) -> C = { raise(lceLoading()) }): C = when (this) {
is Lce.Loading -> {
isLoading.set(true)
ifLoading(partialContent)
}
is Lce.Content -> content
is Lce.Error -> raise(r = this)
}
/**
* Binds the content of this [Lce] instance and handles its state.
* If this is a [Lce.Loading] state, sets the [isLoading] flag to true and returns the partial content.
* If this is a [Lce.Content] state, returns the content.
* If this is a [Lce.Error] state, raises the error.
*
* @return The content of this [Lce] instance.
*/
fun <C : Any> Lce<E, C>.bindOrNull(): C? = when (this) {
is Lce.Loading -> {
isLoading.set(true)
partialContent
}
is Lce.Content -> content
is Lce.Error -> raise(r = this)
}
}
/**
* Creates a [Lce] instance by executing the given [block] within a [LceRaise] context.
*
* @param ifLoading The function to call if the [block] raises a [Lce.Loading] state.
* By default, it creates a new [Lce.Loading] state with the value returned by the [block].
* @param block The block to execute within a [LceRaise] context.
* @return A [Lce] instance representing the result of the [block].
*/
@OptIn(ExperimentalTypeInference::class)
inline fun <E : Any, C : Any> lce(
ifLoading: LceRaise<E>.(C) -> Lce<E, C> = { lceLoading(partialContent = it) },
@BuilderInference block: LceRaise<E>.() -> C,
): Lce<E, C> = recover(
block = {
val raise = LceRaise(raise = this)
val value = block(raise)
if (raise.isLoading.get()) {
ifLoading(raise, value)
} else {
value.lceContent()
}
},
recover = { e: Lce<E, Nothing> -> e },
)

View file

@ -0,0 +1,37 @@
package com.tangem.domain.core.utils
import arrow.core.Either
import com.tangem.domain.core.lce.Lce
import kotlinx.coroutines.flow.Flow
/**
* [Flow] of [Either]
*
* @param E type of left value
* @param A type of right value
* */
typealias EitherFlow<E, A> = Flow<Either<E, A>>
/**
* Converts an [Either] instance to a [Lce] instance.
* If this is a [Either.Left], it is converted to a [Lce.Error] with the same error.
* If this is a [Either.Right], it is converted to a [Lce.Content] or [Lce.Loading] with the same content,
* depending on the [isStillLoading] parameter.
*
* @param isStillLoading A flag indicating whether the content is still loading.
* If true, the [Either.Right] is converted to a [Lce.Loading].
* @return A [Lce] instance containing the same content or error as this [Either],
* and possibly indicating a loading state.
*/
inline fun <reified E : Any, reified T : Any> Either<E, T>.toLce(isStillLoading: Boolean = false): Lce<E, T> {
return when (this) {
is Either.Left -> Lce.Error(value)
is Either.Right -> {
if (isStillLoading) {
Lce.Loading(value)
} else {
Lce.Content(value)
}
}
}
}

View file

@ -0,0 +1,75 @@
package com.tangem.domain.core.utils
import arrow.core.Either
import arrow.core.identity
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.lce.Lce
/**
* Creates a [Lce.Loading] instance with optional partial content.
*
* @param partialContent The partial content that has been loaded so far, if any.
* @return A [Lce.Loading] instance.
*/
fun <C : Any> lceLoading(partialContent: C? = null): Lce<Nothing, C> = Lce.Loading(partialContent)
/**
* Wraps the receiver object in a [Lce.Content] instance.
*
* @return A [Lce.Content] instance containing the receiver object.
*/
fun <C : Any> C.lceContent(): Lce<Nothing, C> = Lce.Content(content = this)
/**
* Wraps the receiver object in a [Lce.Error] instance.
*
* @return A [Lce.Error] instance containing the receiver object.
*/
fun <E : Any> E.lceError(): Lce<E, Nothing> = Lce.Error(error = this)
/**
* Transforms this [Lce] instance by applying the given function into a new [Lce] instance.
*
* @param block The function to apply to the content of this [Lce].
* @return A new [Lce] instance containing the result of applying the function.
* */
inline fun <E : Any, C : Any, T : Any> Lce<E, C>.flatMap(
block: (content: C, isLoading: Boolean) -> Lce<E, T>,
): Lce<E, T> = when (this) {
is Lce.Loading -> partialContent?.let { block(it, true) } ?: lceLoading()
is Lce.Content -> block(content, false)
is Lce.Error -> this
}
/**
* Returns the content of this [Lce] if it's a [Lce.Content] or applies the given functions if it's a [Lce.Loading] or [Lce.Error].
*
* @param ifLoading The function to apply if this is a [Lce.Loading] state.
* @param ifError The function to apply if this is a [Lce.Error] state.
* @return The content of this [Lce] or the result of applying the corresponding function.
*/
inline fun <E : Any, C : Any> Lce<E, C>.getOrElse(ifLoading: (maybeContent: C?) -> C, ifError: (error: E) -> C): C {
return fold(
ifLoading = ifLoading,
ifContent = ::identity,
ifError = ifError,
)
}
/**
* Transforms this [Lce] into an [Either] instance.
* If this is a [Lce.Content], the content is wrapped in a [Either.Right].
* If this is a [Lce.Error], the error is wrapped in a [Either.Left].
* If this is a [Lce.Loading], the [ifLoading] function is applied to the partial content and the result is wrapped in a
* [Either.Right].
*
* @param ifLoading The function to apply if this is a [Lce.Loading] state.
* @return An [Either] instance containing the content or error of this [Lce],
* or the result of applying the [ifLoading] function to the partial content.
*/
inline fun <E : Any, C : Any> Lce<E, C>.toEither(ifLoading: (maybeContent: C?) -> C): Either<E, C> = fold(
ifLoading = { ifLoading(it).right() },
ifContent = { it.right() },
ifError = { it.left() },
)