-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from RedMadRobot/feautre/result-flow
Add resultflow module
- Loading branch information
Showing
8 changed files
with
236 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
## Unreleased | ||
|
||
Initial release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] | ||
|
||
--- | ||
<!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
|
||
- [Installation](#installation) | ||
- [Usage](#usage) | ||
- [Comparing to LCE](#comparing-to-lce) | ||
- [Contributing](#contributing) | ||
|
||
<!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
|
||
A couple of extensions to convert long operations into `Flow<Result<T>>`. | ||
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:<version>") | ||
} | ||
``` | ||
|
||
## Usage | ||
|
||
Use `resultFlow` function to turn long operations into `Flow<Result<T>>`: | ||
|
||
```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<T>` and it is simple to switch between `Result<T>` and `Flow<Result<T>>`. | ||
|
||
## 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
plugins { | ||
convention.library.kotlin | ||
} | ||
|
||
dependencies { | ||
api(kotlin("stdlib")) | ||
api(stack.kotlinx.coroutines.core) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
group=com.redmadrobot.gears | ||
version=0.1.0 | ||
description=A couple of extensions to convert long operations into Flow<Result<T>> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <T> resultFlow(block: suspend () -> Result<T>): Flow<Result<T>> { | ||
return flow { emit(block()) } | ||
} | ||
|
||
/** | ||
* Creates a flow containing a single value – the result of the given [block] wrapped into [Result]. | ||
* @see flow | ||
*/ | ||
public fun <T> resultFlow(block: suspend () -> T): Flow<Result<T>> { | ||
return flow { emit(block()) } | ||
.toResultFlow() | ||
} | ||
|
||
/** Wraps values and errors from [this] flow with [Result]. */ | ||
public fun <T> Flow<T>.toResultFlow(): Flow<Result<T>> { | ||
return map { Result.success(it) } | ||
.catch { emit(Result.failure(it)) } | ||
} | ||
|
||
@Deprecated( | ||
"Call toResultFlow() on Flow<Result<T>> is redundant and can be removed.", | ||
ReplaceWith("this"), | ||
level = DeprecationLevel.ERROR, | ||
) | ||
@JvmName("-redundant_toResultFlow") | ||
public fun <T> Flow<Result<T>>.toResultFlow(): Flow<Result<T>> = this | ||
|
||
/** | ||
* Calls the [Result.fold] on a flow containing [Result]. | ||
* Shorthand for `map { it.fold(...) }` | ||
*/ | ||
public inline fun <T, R : Any> Flow<Result<T>>.foldEach( | ||
crossinline onSuccess: (T) -> R, | ||
crossinline onFailure: (Throwable) -> R, | ||
): Flow<R> { | ||
return map { it.fold(onSuccess, onFailure) } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <T> Flow<Result<T>>.onEachState(action: suspend (ResultState) -> Unit): Flow<Result<T>> { | ||
return onStart { action(Pending) } | ||
.onEach { result -> | ||
val state = result.fold( | ||
onSuccess = { Success }, | ||
onFailure = { Failure(it) }, | ||
) | ||
action(state) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters