diff --git a/README.md b/README.md index 6d6d4fd..b6f7798 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://img.shields.io/github/actions/workflow/status/RedMadRobot/gears-android/main.yml?branch=main&style=flat-square)][ci] [![License](https://img.shields.io/github/license/RedMadRobot/gears-android?style=flat-square)][license] -**Gears** — small libraries used in red_mad_robot to build awesome Android applications. +**Gears** – small libraries used in red_mad_robot to build awesome Android applications. Gears could be used together or alone. --- @@ -19,19 +19,19 @@ Gears could be used together or alone. ## Libraries -### :gear: **[gears](gears/)** +### :gear: **[Gears](gears/)** -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/gears-compose?style=flat-square)][compose-gears] - A set of gears for Jetpack Compose -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square)][kotlin-gears] - A set of gears for Kotlin +- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/gears-compose?style=flat-square)][gears-compose] — A set of gears for Jetpack Compose +- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square)][gears-kotlin] — A set of gears for Kotlin ### :hammer_and_wrench: **[red_mad_robot Android KTX](ktx/)** -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/core-ktx?style=flat-square&label=core-ktx)][core-ktx] - Extensions in addition to androidx core-ktx -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/fragment-ktx?style=flat-square&label=fragment-ktx)][fragment-ktx] - A set of extensions in addition to androidx fragment-ktx -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/fragment-args-ktx?style=flat-square&label=fragment-args-ktx)][fragment-args-ktx] - Delegates for safe dealing with fragments' arguments -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/lifecycle-livedata-ktx?style=flat-square&label=lifecycle-livedata-ktx)][lifecycle-livedata-ktx] - Extended set of extensions for dealing with `LiveData` -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/resources-ktx?style=flat-square&label=resources-ktx)][resources-ktx] - A set of extensions for accessing resources -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/viewbinding-ktx?style=flat-square&label=viewbinding-ktx)][viewbinding-ktx] - A set of extensions for dealing with ViewBinding +- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/core-ktx?style=flat-square&label=core-ktx)][core-ktx] — Extensions in addition to androidx core-ktx +- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/fragment-ktx?style=flat-square&label=fragment-ktx)][fragment-ktx] — A set of extensions in addition to androidx fragment-ktx +- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/fragment-args-ktx?style=flat-square&label=fragment-args-ktx)][fragment-args-ktx] — Delegates for safe dealing with fragments' arguments +- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/lifecycle-livedata-ktx?style=flat-square&label=lifecycle-livedata-ktx)][lifecycle-livedata-ktx] — Extended set of extensions for dealing with `LiveData` +- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/resources-ktx?style=flat-square&label=resources-ktx)][resources-ktx] — A set of extensions for accessing resources +- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/viewbinding-ktx?style=flat-square&label=viewbinding-ktx)][viewbinding-ktx] — A set of extensions for dealing with ViewBinding ### :mag_right: **[ViewModelEvents](viewmodelevents/)** @@ -39,10 +39,14 @@ Gears could be used together or alone. - [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square)][viewmodelevents-flow] - An implementation of ViewModelEvents via `Flow` - [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square)][viewmodelevents-livedata] - An implementation of ViewModelEvents via `LiveData` +### :hourglass_flowing_sand: **[Result Flow](resultflow/)** [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/resultflow?style=flat-square)] + +A couple of extensions to convert long operations into `Flow>`. + ## Why Gears? -The goal of this monorepository is to simplify creation and publication of libraries. -These libraries, which we are calling "gears," are small but important parts of our tech stack that we want to share between our projects. +The goal of this mono-repository is to simplify the creation and publication of libraries. +These libraries, which we're calling "gears," are small but important parts of our tech stack that we want to share between our projects. Libraries may be initially developed here and then moved out from this repository as part of their lifecycle. Large libraries or those with unique build infrastructure should be moved into a separate repository. @@ -50,7 +54,7 @@ Large libraries or those with unique build infrastructure should be moved into a ## Contribution Merge requests are welcome. -For major changes, please open a [discussion][discussions] first to discuss what you would like to change. +For major changes, open a [discussion][discussions] first to discuss what you would like to change. ## License @@ -64,8 +68,8 @@ For major changes, please open a [discussion][discussions] first to discuss what [viewbinding-ktx]: ktx/viewbinding-ktx/ [license]: LICENSE -[compose-gears]: gears/gears-compose -[kotlin-gears]: gears/gears-kotlin +[gears-compose]: gears/gears-compose +[gears-kotlin]: gears/gears-kotlin [viewmodelevents-compose]: viewmodelevents/viewmodelevents-compose/ [viewmodelevents-flow]: viewmodelevents/viewmodelevents-flow/ diff --git a/resultflow/CHANGELOG.md b/resultflow/CHANGELOG.md new file mode 100644 index 0000000..81199b7 --- /dev/null +++ b/resultflow/CHANGELOG.md @@ -0,0 +1,3 @@ +## Unreleased + +Initial release diff --git a/resultflow/README.md b/resultflow/README.md new file mode 100644 index 0000000..8568f3b --- /dev/null +++ b/resultflow/README.md @@ -0,0 +1,93 @@ +# ResultFlow + +[![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/resultflow?style=flat-square)][mavenCentral] +[![License](https://img.shields.io/github/license/RedMadRobot/gears-android?style=flat-square)][license] + +--- + + + +- [Installation](#installation) +- [Usage](#usage) +- [Comparing to LCE](#comparing-to-lce) +- [Contributing](#contributing) + + + +A couple of extensions to convert long operations into `Flow>`. +Allows handling such operations in functional way and provides single point to handle `Pending`, `Success` and `Failure` states. + +## Installation + +Add the dependency: + +```kotlin +repositories { + mavenCentral() +} + +dependencies { + implementation("com.redmadrobot.gears:resultflow:") +} +``` + +## Usage + +Use `resultFlow` function to turn long operations into `Flow>`: + +```kotlin +resultFlow { respository.fetchData() } +``` + +Use `foldEach` to map result value or handle both `Success` and `Failure`: + +```kotlin +resultFlow { respository.fetchData() } + .foldEach( + onSuccess = { handleContent(it) }, + onFailure = { showError(it) }, + ) + +// or + +resultFlow { repository.fetchData() } + .onEach { handleResult(it) } +``` + +Use `onEachState` to handle operation state ([ResultState](src/main/kotlin/ResultState.kt)) in single place: + +```kotlin +resultFlow { repository.fetchData() } + .onEachState { resultState -> + // resultState could be Pending, Success, or Failure + state = state.copy(loading = resultState.isPending) + } +``` + +## Comparing to LCE + +You may notice that the `ResultState` is similar to the pattern LCE (Loading, Content, Error). +Both of these patterns allow handling operations in a functional way, +both of them can be used to handle operation state in a single place. +However, these patterns have different purposes. +The `ResultState` purpose is to **indicate** an operation state, ignoring the result of the operation. +So, `ResultState.Success` doesn't contain any value compared to LCE's Content. +The result of the operation should be handled separately, using `onEach` or `foldEach` functions. + +Here are more reasons why we don't use LCE: + +- In most cases where we've used LCE, it was more convenient to handle `Loading` separately from the final result (`Content` or `Error`), and in some cases, we don't want to handle `Loading` at all. + For such cases it is handy to have separate places to handle operation state and operation result. +- We found it useful to not expose `Loading` state as a return type, but isolate its usage inside the `onEachState` function which is called only when we need to handle this state. +- We don't always want to handle operations in a functional style. + Especially if we need to call several operations one after another, it is more convenient to do it in an imperative style. + In such cases we use `Result` and it is simple to switch between `Result` and `Flow>`. + +## Contributing + +Merge requests are welcome. +For major changes, open an issue first to discuss what you would like to change. + + +[mavenCentral]: https://search.maven.org/artifact/com.redmadrobot.gears/resultflow +[license]: ../LICENSE diff --git a/resultflow/build.gradle.kts b/resultflow/build.gradle.kts new file mode 100644 index 0000000..3a25bea --- /dev/null +++ b/resultflow/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + convention.library.kotlin +} + +dependencies { + api(kotlin("stdlib")) + api(stack.kotlinx.coroutines.core) +} diff --git a/resultflow/gradle.properties b/resultflow/gradle.properties new file mode 100644 index 0000000..641a71e --- /dev/null +++ b/resultflow/gradle.properties @@ -0,0 +1,3 @@ +group=com.redmadrobot.gears +version=0.1.0 +description=A couple of extensions to convert long operations into Flow> diff --git a/resultflow/src/main/kotlin/ResultFlow.kt b/resultflow/src/main/kotlin/ResultFlow.kt new file mode 100644 index 0000000..1980e89 --- /dev/null +++ b/resultflow/src/main/kotlin/ResultFlow.kt @@ -0,0 +1,49 @@ +package com.redmadrobot.gears.resultflow + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map + +/** + * Creates a flow containing a single value – the result returned from the given [block]. + * @see flow + */ +@JvmName("resultFlowResult") +public fun resultFlow(block: suspend () -> Result): Flow> { + return flow { emit(block()) } +} + +/** + * Creates a flow containing a single value – the result of the given [block] wrapped into [Result]. + * @see flow + */ +public fun resultFlow(block: suspend () -> T): Flow> { + return flow { emit(block()) } + .toResultFlow() +} + +/** Wraps values and errors from [this] flow with [Result]. */ +public fun Flow.toResultFlow(): Flow> { + return map { Result.success(it) } + .catch { emit(Result.failure(it)) } +} + +@Deprecated( + "Call toResultFlow() on Flow> is redundant and can be removed.", + ReplaceWith("this"), + level = DeprecationLevel.ERROR, +) +@JvmName("-redundant_toResultFlow") +public fun Flow>.toResultFlow(): Flow> = this + +/** + * Calls the [Result.fold] on a flow containing [Result]. + * Shorthand for `map { it.fold(...) }` + */ +public inline fun Flow>.foldEach( + crossinline onSuccess: (T) -> R, + crossinline onFailure: (Throwable) -> R, +): Flow { + return map { it.fold(onSuccess, onFailure) } +} diff --git a/resultflow/src/main/kotlin/ResultState.kt b/resultflow/src/main/kotlin/ResultState.kt new file mode 100644 index 0000000..97a8298 --- /dev/null +++ b/resultflow/src/main/kotlin/ResultState.kt @@ -0,0 +1,60 @@ +package com.redmadrobot.gears.resultflow + +import com.redmadrobot.gears.resultflow.ResultState.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart + +/** + * Represents three possible states during a result pending. + * + * - [Pending] – A result that is still pending and has not yet completed. + * - [Success] – A successful result. + * - [Failure] – A failed result, contains the exception that caused the failure. + * + * @see onEachState + */ +public sealed interface ResultState { + + /** Shorthand for `status is ResultState.Pending`. */ + public val isPending: Boolean + get() = this is Pending + + /** Shorthand for `status is ResultState.Success`. */ + public val isSuccess: Boolean + get() = this is Success + + /** Shorthand for `status is ResultState.Failure`. */ + public val isFailure: Boolean + get() = this is Failure + + /** Returns an exception */ + public fun exceptionOrNull(): Throwable? = if (this is Failure) exception else null + + /** Represents a result that is still pending and has not yet completed. */ + public data object Pending : ResultState + + /** Represents a successful result. */ + public data object Success : ResultState + + /** Represents a failed result. Contains the [exception] that caused the failure */ + public data class Failure(val exception: Throwable) : ResultState + + /** Extension point to give an ability to create extension-functions on a companion object. */ + public companion object +} + +/** + * Returns the flow that invokes the given [action] on each [ResultState] of this flow. + * It always calls the [action] passing the [ResultState.Pending] first. + */ +public fun Flow>.onEachState(action: suspend (ResultState) -> Unit): Flow> { + return onStart { action(Pending) } + .onEach { result -> + val state = result.fold( + onSuccess = { Success }, + onFailure = { Failure(it) }, + ) + action(state) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0b916ca..9b39368 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -40,4 +40,5 @@ include( ":viewmodelevents:viewmodelevents-compose", ":viewmodelevents:viewmodelevents-flow", ":viewmodelevents:viewmodelevents-livedata", + ":resultflow", )