Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resultflow module #56

Merged
merged 3 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand All @@ -19,38 +19,42 @@ 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/)**

- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square)][viewmodelevents-compose] - A set of extensions for dealing with ViewModelEvents inside `@Composable` functions
- [![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<Result<T>>`.

## 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.

## 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

Expand All @@ -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/
Expand Down
3 changes: 3 additions & 0 deletions resultflow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Unreleased

Initial release
93 changes: 93 additions & 0 deletions resultflow/README.md
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
8 changes: 8 additions & 0 deletions resultflow/build.gradle.kts
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)
}
3 changes: 3 additions & 0 deletions resultflow/gradle.properties
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>>
49 changes: 49 additions & 0 deletions resultflow/src/main/kotlin/ResultFlow.kt
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) }
}
60 changes: 60 additions & 0 deletions resultflow/src/main/kotlin/ResultState.kt
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)
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ include(
":viewmodelevents:viewmodelevents-compose",
":viewmodelevents:viewmodelevents-flow",
":viewmodelevents:viewmodelevents-livedata",
":resultflow",
)