From f7f572e37cd713bcd842f2cabe639cc8354654c1 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Thu, 18 Jul 2024 18:06:56 +0200 Subject: [PATCH 1/3] resultflow: Add initial implementation --- resultflow/CHANGELOG.md | 3 ++ resultflow/README.md | 57 +++++++++++++++++++++ resultflow/build.gradle.kts | 8 +++ resultflow/gradle.properties | 3 ++ resultflow/src/main/kotlin/ResultFlow.kt | 49 ++++++++++++++++++ resultflow/src/main/kotlin/ResultState.kt | 60 +++++++++++++++++++++++ settings.gradle.kts | 1 + 7 files changed, 181 insertions(+) create mode 100644 resultflow/CHANGELOG.md create mode 100644 resultflow/README.md create mode 100644 resultflow/build.gradle.kts create mode 100644 resultflow/gradle.properties create mode 100644 resultflow/src/main/kotlin/ResultFlow.kt create mode 100644 resultflow/src/main/kotlin/ResultState.kt 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..415ad9a --- /dev/null +++ b/resultflow/README.md @@ -0,0 +1,57 @@ +# 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) +- [Contributing](#contributing) + + + +Utilities to deal with `Flow>`. + +## Installation + +Add the dependency: + +```kotlin +repositories { + mavenCentral() +} + +dependencies { + implementation("com.redmadrobot.gears:resultflow:") +} +``` + +## Usage + +> [!WARNING] +> The documentation is under construction + +Tracking request status and handling error: + +```kotlin +resultFlow { repository.fetchData() } + .onEachState { resultState -> + state = state.copy(loading = resultState.isLoading) + } + .foldEach( + onSuccess = { handleContent(it) }, + onFailure = { showError(it) }, + ) +``` + +## 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", ) From 37fcaa1a8c543bf42016b69e509d17753188a53f Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Tue, 30 Jul 2024 11:10:03 +0200 Subject: [PATCH 2/3] resultflow: Add readme --- README.md | 4 ++++ resultflow/README.md | 52 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6d6d4fd..8657914 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ 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. diff --git a/resultflow/README.md b/resultflow/README.md index 415ad9a..8568f3b 100644 --- a/resultflow/README.md +++ b/resultflow/README.md @@ -9,11 +9,13 @@ - [Installation](#installation) - [Usage](#usage) +- [Comparing to LCE](#comparing-to-lce) - [Contributing](#contributing) -Utilities to deal with `Flow>`. +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 @@ -31,22 +33,56 @@ dependencies { ## Usage -> [!WARNING] -> The documentation is under construction +Use `resultFlow` function to turn long operations into `Flow>`: -Tracking request status and handling error: +```kotlin +resultFlow { respository.fetchData() } +``` + +Use `foldEach` to map result value or handle both `Success` and `Failure`: ```kotlin -resultFlow { repository.fetchData() } - .onEachState { resultState -> - state = state.copy(loading = resultState.isLoading) - } +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. From 0e71bbbf6e38871e658d3bb1d38769d37e35627d Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Tue, 30 Jul 2024 11:29:56 +0200 Subject: [PATCH 3/3] docs: Stylistic changes in readme --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8657914..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/)** @@ -45,8 +45,8 @@ 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. @@ -54,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 @@ -68,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/