Giter Site home page Giter Site logo

sergejsha / knot Goto Github PK

View Code? Open in Web Editor NEW
241.0 9.0 21.0 1.47 MB

Unidirectional reactive state container for Android & Kotlin.

License: Apache License 2.0

Kotlin 100.00%
kotlin kotlin-android android mvi-android mvi-architecture android-architecture unidirectional-data-flow

knot's Introduction

Maven Central maintenance-status License

๐Ÿงถ Knot

Concise reactive state container library for Android applications.

Concept

Knot helps managing application state by reacting on events and performing asynchronous actions in a structured way. There are five core concepts Knot defines: State, Change, 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.

Change is an immutable data object with an optional payload intended for changing the State. A Change 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 Change.

Reducer is a function that takes the previous State and a Change as arguments and returns the new State and an optional Action 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 an optional Action. If Action is present, Knot will perform it and provide resulting Change (if any) back to the Reducer.

In addition to that each Knot can subscribe to Events coming from external sources and turn them into Changes for further processing.

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 Changes and Actions in console.

sealed class State {
   object Initial : State()
   object Loading : State()
   data class Content(val data: String) : State()
   data class Failed(val error: Throwable) : State()
}

sealed class Change {
   object Load : Change() {
      data class Success(val data: String) : Change()
      data class Failure(val error: Throwable) : Change()
   }
}

sealed class Action {
   object Load : Action()
}

val knot = knot<State, Change, Action> {
    state { 
        initial = State.Initial 
    }
    changes {
        reduce { change ->
            when (change) {
                is Change.Load -> State.Loading + Action.Load
                is Change.Load.Success -> State.Content(data).only
                is Change.Load.Failure -> State.Failed(error).only
            }
        }
    }
    actions {
        perform<Action.Load> {
            switchMapSingle<String> { 
                loadData()
                    .map<Change> { Change.Load.Success(it) }
                    .onErrorReturn { Change.Load.Failure(it) }
            }
        }
    }
    events {
        source {
            dataChangeObserver.signal.map { Change.Load }
        }
    }
}

val states = knot.state.test()
knot.change.accept(Change.Load)

states.assertValues(
    State.Initial,
    State.Loading,
    State.Content("data")
)

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 .only 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 Changes, 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. Once all Delegates are defined, they can be composed together and provided to CompositeKnot which implements standard Knot interface. For more information check out Composite ViewModel post.

Documentation

  1. Knot Sample App is the first place to look at.
  2. Async Actions to learn how to perform and cancel asynchronous actions.
  3. External Events to learn how to observe and handle external events.
  4. Terminal events in Actions section
  5. Composite ViewModel to learn more about composition.
  6. Troubleshooting

Other examples

Why Knot?

  • 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.
  • Production ready - Knot is used in production.
  • Why not?

RxJava3 Binaries Maven Central

allprojects {
    repositories {
        mavenCentral()
    }
}
dependencies {
    implementation "de.halfbit:knot3:<version>"
    
    // Because Knot is not released for each and every RxJava version, 
    // it is recommended you also explicitly depend on RxJava's latest 
    // version for bug fixes and new features.
    implementation 'io.reactivex.rxjava3:rxjava:3.0.4'    
}

RxJava2 Binaries Maven Central

allprojects {
    repositories {
        mavenCentral()
    }
}
dependencies {
    implementation "de.halfbit:knot:<version>"
    
    // Becase Knot is not released for each and every RxJava version, 
    // it is recommended you also explicitly depend on RxJava's latest 
    // version for bug fixes and new features.
    implementation 'io.reactivex.rxjava2:rxjava:2.2.19'
}

Inspiration

Knot was inspired by two awesome projects

License

Copyright 2019, 2020 Sergej Shafarenka, www.halfbit.de

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.

knot's People

Contributors

gustavkarlsson avatar hamen avatar realdadfish avatar sergejsha avatar stefma avatar wowselim avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

knot's Issues

Change interceptor in Prime fails with ClassCastException

Given:

  • CompositeKnot with two Primes - PrimeA and PrimeB
  • PrimeA declares ChangeA and PrimeB declares ChangeB
  • PrimeA has change { intercept { } } section

When:

  • PrimeB dispatches ChangeB

Actual:

  • knot.state terminates with ClassCastException because PrimeA tries to cast ChangeB to ChangeA

Expected:

  • ChangeB stays private to PrimeB
  • knot.state doesn't terminate
  • change { intercept { } } section declared for whole CompositeKnot is capable of intercepting both change types

Actions broken in rxjava2-3.0.2

Something with actions seems broken in 3.0.2 and I believe it could be this flatMap:
fd78d39#diff-6eee9d229ac30d0f2976e4d1a66ffae4R285

My perform<Action> observables keep stacking on top of each other for each new action emitted, causing infinite streams in switchMaps to never be disposed.

Example:

perform<Action> {
    switchMap {
        Observable.never<Change>()
            .doOnLifecycle({
                // onSubscribe gets called for every new emitted action (as it should)
            }, {
                // onDispose never gets called (but it should get called for every switch)
            })
    }
}

CompositeKnot.state will be called even if the same state will be emitted

Given this test:

    @Test
    fun compositeKnotTest() {
        data class X(val x: Int = 0)

        val compositeKnot = compositeKnot<X> {
            this.state {
                initial = X()
            }
        }
        compositeKnot.registerPrime<Unit, Action> {
            changes {
                reduce<Unit> {
                    return@reduce X().only
                }
            }
            events {
                source {
                    compositeKnot.state
                        .doOnNext { println(it) }
                        .map { Unit }
                }
            }
        }
        compositeKnot.compose()
        compositeKnot.change.accept(Unit)
        Thread.sleep(3000)
    }

I would expect that after I call compositeKnot.change.accept(Unit), the reducer get called, which creates a new X() and that's it.
Why don't I expect a call to doOnNext?
Because X is a data class and 100% equal to the previous state.
But what happen currently?
We run into an endless loop.
After the reducer creates a new X() the compositeKnot.state get called, return a new Change which will call the reducer which creates a new X() which calls the compositeKnot.state.... recursively...

As I said, I would expect (because the new X is equal to the new X) the compositeKnot.state would not be triggered.

Make the KnotBuilder (or DSL) more extensible

We want to log each state, change and action changes.
In the past we simply used something like this to print it to logcat:

        knot<A,B,C> {
            state {
                watchAll { TAG.logDebug { "|  state: $it" } }
                initial = ...
            }
            changes {
                watchAll {
                    TAG.logDebug { "+----------------" }
                    TAG.logDebug { "| change: $it" }
                }
                reduce { ... }
            }
            actions {
                watchAll { TAG.logDebug { "| action: $it" } }
                perform<...> { ... }
            }
        }

But we don't want to repeat these log statements in every class were we use knot.
So we thought about another implementation like a fun <A,B,C> loggableKnot<A,B,C>(block: ...): KnotBuilder<A,B,C> But this is not going to work because each time we call the state, change or action lambda a new instance of the StateBuilder resp. ChangeBuilder and ActionBuilder will be created.

That means it is currently not possible to override (or better add) "something" to these builders "behind the scene".

The only solution we found was to have a extension function for each Builder (State, Change, Action):
Example:

fun <State : Any> StateBuilder<State>.logState(tag: String) {
    watchAll { tag.logInfo { "|  state: $it" } }
}

But this looks of course not so nice in the knot DSL:

        knot<A,B,C> {
            state {
                logState(TAG)
                initial = ...
            }
            changes {
                logChanges(TAG)
                reduce { ... }
            }
            actions {
                logActions(TAG)
                perform<...> { ... }
            }
        }

So what I want to have is something like this:

fun <State : Any, Change : Any, Action : Any> KnotBuilder<State, Change, Action>.logEverything(tag: String) {
    state {
        watchAll { tag.logInfo { "|  state: $it" } }
    }
    changes {
        watchAll {
            tag.logInfo { "+----------------" }
            tag.logInfo { "| change: $it" }
        }
    }
    actions {
        watchAll { tag.logDebug { "| action: $it" } }
    }
}

But again, these will override each StateBuilder, ChangesBuilder and ActionsBuilder.. But instead of overriding I want to "add" these things..

Or is there a another/better solution for something like this?

RxJava should be declared as a api dependency

Currently the dependency to rxjava is an implementation dependency.
That means it is not available for consumers of this library at compile time.
But you have open APIs to rxjava (like the knot.state property).
That means if a project wants to use knot and declare it as a dependency it will not work.
It will lead to compile time errors.

Long story short:
Rxjava should be declared as an api dependency so other project can directly start with knot without adding rxjava to their dependencies.

State observable silently terminates on error

In CompoisiteKnot (should also be valid for Knot) without any subscribers of the state, when a reducer throws an exception, the exception gets silently ignored and whole knot instance stops processing changes.

Solution: any exception thrown in the reduce function should lead to RxJavaPlugins.onError() call and consequent crash.

Impacted version: 3.1.0 (rx2 and rx3)

Actions are not emitted after onErrorReturn returns a value

In the snippet below

     actions {
           perform<Action.Load> {
               flatMapSingle<Payload> { api.load() }
                   .map<Change> { Change.Load.Success(it) }
                   .onErrorReturn { Change.Load.Failure(it) }
           }
     }

Action.Load is not emitted once Change.Load.Failure(it) is returned.

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.