Giter Club home page Giter Club logo

reduce's Introduction

License

Reduce

Concise reactive state container library for Android applications based on Kotlin coroutines.

Inspiration

Reduce was inspired by awesome project

But it is written for RxJava. I decided to write it based on Kotlin corutines.

Concept

Reduce helps managing application state by reacting on events and performing asynchronous actions in a structured way. There are five core concepts Knot defines: State, Intent, Action, Reducer and Effect.

State represents an immutable state of an application. It can be a state of a screen or a state of an internal statefull headless component.

Intent is an immutable data object with an optional payload intended for changing the State. A Intent can be produced from an external source or be a result of execution of an Action.

Action is a synchronous or an asynchronous operation which, when completed, can – but doesn't have to – emit a new Intent.

Reducer is a function that takes the previous State and a Intent as arguments and returns the new State and an optional Actions wrapped by the Effect class. Reducer in Knot is designed to stay side-effects free because each side-effect can be turned into an Action and returned from the reducer function together with a new state in a pure way.

Effect is a convenient wrapper class containing the new State and a list of Actions. If list of Actions is not empty, Knot will perform it and provide resulting Intent (if any) back to the Reducer.

Getting Started

The example below declares a Knot capable of loading data, handling Success and Failure loading results and reloading data automatically when an external "data changed" signal gets received. It also logs all State mutations as well as all processed Intents and Actions in console.

sealed class BooksState : State {
    object Empty : BooksState()
    object Loading : BooksState()
    data class Content(val books: List<Book>) : BooksState()
    data class BooksError(val message: String) : BooksState()
}

sealed class BooksIntent : StateIntent {
    object Load : BooksIntent()
    class Success(val books: List<Book>) : BooksIntent()
    class Failure(val message: String) : BooksIntent()
}

sealed class BooksAction : Action {
    object Load : BooksAction()
}

val knot = knot<BooksState, BooksIntent, BooksAction> {

    initialState = BooksState.Empty

    reduce { intent ->
        when (intent) {
            BooksIntent.Load -> when (this) {
                BooksState.Empty,
                is BooksState.Content,
                is BooksState.BooksError -> BooksState.Loading + BooksAction.Load
                else -> stateOnly
            }
            is BooksIntent.Success -> when (this) {
                BooksState.Loading -> BooksState.Content(intent.books).stateOnly
                else -> unexpected(intent)
            }
            is BooksIntent.Failure ->  when (this) {
                BooksState.Loading -> BooksState.BooksError(intent.message).stateOnly
                else -> unexpected(intent)
            }
        }
    }

    actions { action ->
        when (action) {
            BooksAction.Load -> {
                repository.loadBooks().toIntent()
            }
        }
    }
}

Notice how inside the reduce function a new State can be combined with an Action using + operator. If only the State value should be returned from the reducer, the .stateOnly suffix is added to the State.

Composition

If your knot becomes complex and you want to improve its readability and maintainability, you may consider to write a composite knot. You start composition by grouping related functionality into, in a certain sense, indecomposable pieces called Delegates. Each Delegate is isolated from the other Delegates. It defines its own set of Intents, Actions and Reducers. It's only the State, what is shared between the Delegates. In that respect each Delegate can be seen as a separate Knot working on a shared State.

    private val commonState = CoroutineKnotState<BooksState>(BooksState.Empty)

    private val clearKnot = knot<BooksState, ClearBookIntent, ClearBooksAction> {

        knotState = commonState

        reduce { intent ->
            when (intent) {
                ClearBookIntent.Clear -> when (this) {
                    is BooksState.Content -> BooksState.Empty.stateOnly
                    is BooksState.Empty -> stateOnly
                    else -> unexpected(intent)
                }
            }
        }
    }

    private val loadKnot = knot<BooksState, BooksIntent, BooksAction> {

        knotState = commonState

        reduce { intent ->
            when (intent) {
                BooksIntent.Load -> when (this) {
                    BooksState.Empty,
                    is BooksState.Content,
                    is BooksState.BooksError -> BooksState.Loading + BooksAction.Load
                    else -> stateOnly
                }
                is BooksIntent.Success -> when (this) {
                    BooksState.Loading -> BooksState.Content(intent.books).stateOnly
                    else -> unexpected(intent)
                }
                is BooksIntent.Failure ->  when (this) {
                    BooksState.Loading -> BooksState.BooksError(intent.message).stateOnly
                    else -> unexpected(intent)
                }
            }
        }

        actions { action ->
            when (action) {
                BooksAction.Load -> {
                    repository.loadBooks().toIntent()
                }
            }
        }
    }

Scalability

Through 'performActions()' part single knots can be combined together.

Simplify

Raviola in his article A case against the MVI architecture pattern raises issue about MVI pattern: hard to read code, you have to jump from reducer part to actions part and back. This library help to resolve the issue. Here you can join intent reduce with side effect action in one place.

...
 	easyKnot<BooksState, BooksIntent, BooksAction> {

        knotState = commonState

        reduce { intent ->
            when (intent) {
                BooksIntent.Load -> when (this) {
                    BooksState.Empty,
                    is BooksState.Content,
                    is BooksState.BooksError -> BooksState.Loading + SideEffect<BookIntent> {
						repository.loadBooks().toIntent()
					}
                    else -> stateOnly
                }
                is BooksIntent.Success -> when (this) {
                    BooksState.Loading -> BooksState.Content(intent.books).stateOnly
                    else -> unexpected(intent)
                }
                is BooksIntent.Failure ->  when (this) {
                    BooksState.Loading -> BooksState.BooksError(intent.message).stateOnly
                    else -> unexpected(intent)
                }
            }
        }
...

Even several actions can be executed:

    easyKnot<TrafficState, TrafficIntent> {
       initialState = TrafficState.Off

       reduce { intent ->
           when (intent) {
               TrafficIntent.Minus -> {
                   if (cars > 0) {
                       cars--
                       this + startMovement()
                   } else {
                       stateOnly
                   }
               }
               TrafficIntent.Plus -> {
                   cars++
                   this + startMovement()
               }
               TrafficIntent.Off -> TrafficState.Off + close()
               TrafficIntent.On -> TrafficState.On + open() + startMovement()
           }
       }
   }

   private fun open() = SideEffect<TrafficIntent> {
       open.set(true)
       null
   }

   private fun close() = SideEffect<TrafficIntent> {
       open.set(false)
       null
   }

   private fun startMovement() = SideEffect<TrafficIntent> {
       if (moving.get()) return@SideEffect null
       moving.set(true)
       while (open.get() && cars > 0) {
           streetIn?.carOut()
           streetOut?.carIn()
           cars--
       }
       moving.set(false)
       null
   }

Gradle

Step 1. Add the JitPack repository to your build file. Add it in your root build.gradle at the end of repositories:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Step 2. Add the dependency

dependencies {
    implementation 'com.github.genaku:Reduce:reduce-core:<version>'  // core of Reduce lib
    implementation 'com.github.genaku:Reduce:reduce-ext:<version>'   // Android extensions to connect reduce knots to activity/fragment lifecycle
}

Why Reduce?

  • Predictable - state is the single source of truth.
  • Side-effect free reducer - by design.
  • Scalable - single knots can be combined together to build more complex application logic.
  • Composable - complex knots can be composed out of delegates grouped by related functionality.
  • Structured - easy to read and write DSL for writing better structured and less buggy code.
  • Concise - it has minimalistic API and compact implementation.
  • Testable - reducers and transformers are easy to test.
  • Why not?

License

Copyright 2021 Gennadiy Kuchergin

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

reduce's People

Contributors

genaku avatar

Stargazers

吴上阿吉 avatar Art Shendrik avatar  avatar wangyl avatar Pavel Belovol avatar

Watchers

James Cloos avatar  avatar

Forkers

1gravity

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.