Unidirectional Data Flow in Kotlin - Port of https://github.com/ReSwift/ReSwift to Kotlin

Related tags

ReKotlin
Overview

ReKotlin

License MIT Build Status Download

Port of ReSwift to Kotlin, which corresponds to ReSwift/4.0.0

Introduction

ReKotlin is a Redux-like implementation of the unidirectional data flow architecture in Kotlin. ReKotlin helps you to separate three important concerns of your app's components:

  • State: in a ReKotlin app the entire app state is explicitly stored in a data structure. This helps avoid complicated state management code, enables better debugging and has many, many more benefits...
  • Views: in a ReKotlin app your views update when your state changes. Your views become simple visualizations of the current app state.
  • State Changes: in a ReKotlin app you can only perform state changes through actions. Actions are small pieces of data that describe a state change. By drastically limiting the way state can be mutated, your app becomes easier to understand and it gets easier to work with many collaborators.

The ReKotlin library is tiny - allowing users to dive into the code, understand every single line and hopefully contribute.

About ReKotlin

ReKotlin relies on a few principles:

  • The Store stores your entire app state in the form of a single data structure. This state can only be modified by dispatching Actions to the store. Whenever the state in the store changes, the store will notify all observers.
  • Actions are a declarative way of describing a state change. Actions don't contain any code, they are consumed by the store and forwarded to reducers. Reducers will handle the actions by implementing a different state change for each action.
  • Reducers provide pure functions, that based on the current action and the current app state, create a new app state

For a very simple app, that maintains a counter that can be increased and decreased, you can define the app state as following:

data class AppState (
        val counter: Int = 0
): StateType

You would also define two actions, one for increasing and one for decreasing the counter. For the simple actions in this example we can define empty data classes that conform to action:

data class CounterActionIncrease(val unit: Unit = Unit): Action
data class CounterActionDecrease(val unit: Unit = Unit): Action

Your reducer needs to respond to these different action types, that can be done by switching over the type of action:

fun counterReducer(action: Action, state: AppState?): AppState {
    // if no state has been provided, create the default state
    var state = state ?: AppState()

    when(action){
        is CounterActionIncrease -> {
            state = state.copy(counter = state.counter + 1)
        }
        is CounterActionDecrease -> {
            state = state.copy(counter = state.counter - 1)
        }
    }

    return state
}

In order to have a predictable app state, it is important that the reducer is always free of side effects, it receives the current app state and an action and returns the new app state.

To maintain our state and delegate the actions to the reducers, we need a store. Let's call it mainStore and define it as a global constant, for example in the Main Activity file:

val mainStore = Store(
     reducer = ::counterReducer,
     state = null
)

class MainActivity : AppCompatActivity(){
	//...
}

Lastly, your view layer, in this case an activity, needs to tie into this system by subscribing to store updates and emitting actions whenever the app state needs to be changed (assuming that snake_case View properties are coming from Kotlin Android Extensions):

class MainActivity : AppCompatActivity(), StoreSubscriber<AppState> {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // when either button is tapped, an action is dispatched to the store
        // in order to update the application state
        button_up.setOnClickListener {
            mainStore.dispatch(CounterActionIncrease())
        }
        button_down.setOnClickListener {
            mainStore.dispatch(CounterActionDecrease())
        }

        // subscribe to state changes
        mainStore.subscribe(this)
    }

    override fun newState(state: AppState) {
        // when the state changes, the UI is updated to reflect the current state
        counter_label.text = "${state.counter}"
    }
}

The newState method will be called by the Store whenever a new app state is available, this is where we need to adjust our view to reflect the latest app state.

When working with multiple states in a single class, BlockSubscriber can be used for listening to states in it's specific closure instead of using StoreSubscriber<>

class MainActivity : AppCompatActivity() {

    private val counterLabel: TextView by lazy {
        this.findViewById(R.id.counter_label) as TextView
    }

    private val buttonUp: Button by lazy {
        this.findViewById(R.id.button) as Button
    }

    private val buttonDown: Button by lazy {
        this.findViewById(R.id.button2) as Button
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val appStateSubscriber = BlockSubscriber<AppState> { appState ->
            this.counterLabel.text = "${appState.counter}"
        }

        // when either button is tapped, an action is dispatched to the store
        // in order to update the application state
        this.buttonUp.setOnClickListener {
            mainStore.dispatch(CounterActionIncrease())
        }
        this.buttonDown.setOnClickListener {
            mainStore.dispatch(CounterActionDecrease())
        }

        // subscribe to state changes
        mainStore.subscribe(appStateSubscriber)
    }
}

Button taps result in dispatched actions that will be handled by the store and its reducers, resulting in a new app state.

This is a very basic example that only shows a subset of ReKotlin's features, read the Getting Started Guide (not ported yet) to see how you can build entire apps with this architecture.

You can also watch this talk on the motivation behind the original ReSwift.

Examples

Why ReKotlin?

Model-View-Controller (MVC) is not a holistic application architecture. Typical apps defer a lot of complexity to controllers since MVC doesn't offer other solutions for state management, one of the most complex issues in app development.

Apps built upon MVC often end up with a lot of complexity around state management and propagation. We need to use callbacks, delegations, Key-Value-Observation and notifications to pass information around in our apps and to ensure that all the relevant views have the latest state.

This approach involves a lot of manual steps and is thus error prone and doesn't scale well in complex code bases.

It also leads to code that is difficult to understand at a glance, since dependencies can be hidden deep inside of view controllers. Lastly, you mostly end up with inconsistent code, where each developer uses the state propagation procedure they personally prefer. You can circumvent this issue by style guides and code reviews but you cannot automatically verify the adherence to these guidelines.

ReKotlin attempts to solve these problems by placing strong constraints on the way applications can be written. This reduces the room for programmer error and leads to applications that can be easily understood - by inspecting the application state data structure, the actions and the reducers.

This architecture provides further benefits beyond improving your code base:

  • Stores, Reducers, Actions and extensions such as ReKotlin Router are entirely platform independent - you can easily use the same business logic and share it between apps for multiple platforms
  • Want to collaborate with a co-worker on fixing an app crash? Use (port not yet available) ReSwift Recorder to record the actions that lead up to the crash and send them the JSON file so that they can replay the actions and reproduce the issue right away.
  • Maybe recorded actions can be used to build UI and integration tests?

The ReKotlin tooling is still in a very early stage, but aforementioned prospects excite us and hopefully others in the community as well!

Getting Started Guide

The Getting Started Guide has not yet been ported. In the meantime, please refer to original ReSwift's: Getting Started Guide that describes the core components of apps built with ReSwift.

To get an understanding of the core principles we recommend reading the brilliant redux documentation.

Installation

dependencies {
    implementation 'org.rekotlin:rekotlin:1.0.4'
}

Differences with ReSwift

Dereferencing subscribers will not result in subscription removed

In ReSwift when you dereference the subscriber or it goes out of the scope, you won't receive new state updates.

var subscriber: TestSubscriber? = TestSubscriber()
store.subscribe(subscriber!)
subscriber = nil

However in ReKotlin you need make sure you have unsubscribed explicitly.

val subscriber = TestSubscriber()
store.subscribe(subscriber)
store.unsubscribe(subscriber)

Equatability and skipRepeats

When subscribing without substate selection like store.subscribe(someSubscriber) in swift you need to have your state implementing Equatable in order to skipRepeats being applied automatically.

public struct State: StateType {
    public let mapState: MapState
    public let appState: AppState
}

extension State: Equatable {
    public static func ==(lhs: State, rhs: State) -> Bool {
        //...
    }
}

However in Kotlin(JVM) every object implements equals(), so that skipRepeats will be applied automatically when you store.subscribe(someSubscriber), with Kotlin Structural Equality check used.

Please note, if you implement your states/substates with data classes, Kotlin compiler will automatically derive non-shallow equals() from all properties declared in the primary constructor.

If you want to opt-out of this behaviour please set automaticallySkipRepeats to false in your store declaration:

val store = Store(
	reducer::handleAction, 
	state, 
	automaticallySkipRepeats = false)

Subscribe/Unsubscribe during newState

Under the hood ReKotlin uses a CopyOnWriteArrayList to manage subscriptions (see PR 29 for more details). This implementation detail means that the number of concurrent writes to the subscriptions should be less than the number of concurrent reads.

In terms of using the library this means that un/subscribing may incur a performance overhead if done during newState in store subscribers. We recommend to restrict this usage (concurrent write while reading subscriptions) as much as possible, i.e. avoid subscribe or unsubscribe in calls to newState.

Contributing

Please format your code using kotlinFormatter.xml file from here and then running ./gradlew spotlessApply

Using this code formatter will help us maintain consistency in code style.

Credits

  • Many thanks to Benjamin Encz and other ReSwift contributors for building original ReSwift that we really enjoyed working with.
  • Also huge thanks to Dan Abramov for building Redux - all ideas in here and many implementation details were provided by his library.

Additional Note

This was an attempt to bring redux architecture parity between iOS and Android

Issues
  • middlewares and reducers notifications

    middlewares and reducers notifications

    Hi! I happen to be developing an app using this library (thanks for this, btw!) and, after a thorough debugging process, I have spotted that all my reducers and middlewares get called on every new Action dispatch.

    Thus, regardless which Activity or Fragment I am, any store.dispatch(MyAction()) triggers all reducer and middleware instances (I guess this means that I could even define all reducers and middlewares in the same file). Apart from finding this a bit inefficient, I wonder whether this is the only way to do it or there is a smarter approach.

    Thanks and regards,

    question 
    opened by pablodeafsapps 9
  • Fix/27 concurrent modification

    Fix/27 concurrent modification

    As described in #27 any call to subscribe or unsubscribe from a newState would create a ConcurrentModificationException because the list of subscriptions was modified (by un/subscribe) while iterating over it (from the setter of _state).

    By using a CopyOnWriteArrayList the iteration over the original list finishes (without modification) while any future access of subscription reflects the mutation.

    Fixes #27

    opened by NemoOudeis 8
  • un/subscribe during `newState` callback causes crash -> ConcurrentModificationException

    un/subscribe during `newState` callback causes crash -> ConcurrentModificationException

    Hi,

    I'm toying with ReKotlin and ran into a problem: I have an Android app with a single Activity, that activity displays different screens by attaching and detaching views from its own view (just a container).

    To navigate between different screens I dispatch actions → trigger a state change (the current route changes) → the activity detaches old screen & attaches new screen Each of these screens subscribes to the store.

    So I get the following sequence:

    activity        store
        | subscribe() |
        |------------>|
        |             |  dispatch()
        |             |<----------------
        | newState()  |
       ||<------------|  
       ||             |   create
       ||-------------|--------------> screen
       ||             | subscribe()      |
       ||             |<-----------------|
       ||<------------|
        |             |
        |             |
        💥            💥
    

    this results in a ConcurrentModificationException, full stacktrace:

    java.util.ConcurrentModificationException
            at java.util.ArrayList$Itr.next(ArrayList.java:831)
            at org.rekotlin.Store.set_state(Store.kt:163)
            at org.rekotlin.Store._defaultDispatch(Store.kt:129)
            at org.rekotlin.Store$dispatchFunction$1.invoke(Store.kt:51)
            at org.rekotlin.Store$dispatchFunction$1.invoke(Store.kt:27)
            at my.redux.playground.MainActivityKt$loggingMiddleware$1$1$1.invoke(MainActivity.kt:303)
            at my.redux.playground.MainActivityKt$loggingMiddleware$1$1$1.invoke(MainActivity.kt)
            at org.rekotlin.Store.dispatch(Store.kt:133)
            at my.redux.playground.ScreenOne$4.invoke(MainActivity.kt:108)
            at my.redux.playground.ScreenOne$4.invoke(MainActivity.kt:92)
            at my.redux.playground.MainActivityKt$doOnClick$1.onClick(MainActivity.kt:329)
            at android.view.View.performClick(View.java:5637)
            at android.view.View$PerformClick.run(View.java:22429)
            at android.os.Handler.handleCallback(Handler.java:751)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:154)
            at android.app.ActivityThread.main(ActivityThread.java:6119)
            at java.lang.reflect.Method.invoke(Native Method)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
    

    The problem seems to be caused by calling Store#subscribe or Store#unsubscribe during execution of state updates (in Store.kt lines 39-41 ). The list of subscriptions` is mutated while iterated over.

    My assumption was that subscribing and unsubscribing is independent of state updates and expected that a subscription would only get "active" after/inbetween state changes. Is my assumption wrong? Or is this a bug? Am i using ReKotlin in an unexpected way - if so is there a better approach?

    Thanks guys 🙏

    opened by NemoOudeis 5
  • Gradle conflict

    Gradle conflict

    should this library be working when we use dagger injection? I am not able to build the project now.

    Got this: Error:Exception in thread "main" java.lang.NullPointerException: Couldn't find outer class org/rekotlinrouter/Router$routingSerailActionHandler$1$3 of org/rekotlinrouter/Router$routingSerailActionHandler$1$3$1 at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:1079) at com.google.devtools.build.android.desugar.ClassVsInterface.isOuterInterface(ClassVsInterface.java:56) at com.google.devtools.build.android.desugar.InterfaceDesugaring.visitOuterClass(InterfaceDesugaring.java:246) at org.objectweb.asm.ClassReader.accept(ClassReader.java:638) at org.objectweb.asm.ClassReader.accept(ClassReader.java:500) at com.google.devtools.build.android.desugar.Desugar.desugarClassesInInput(Desugar.java:477) at com.google.devtools.build.android.desugar.Desugar.desugarOneInput(Desugar.java:361) at com.google.devtools.build.android.desugar.Desugar.desugar(Desugar.java:314) at com.google.devtools.build.android.desugar.Desugar.main(Desugar.java:711)

    FAILURE: Build failed with an exception.

    • What went wrong: Execution failed for task ':app:transformClassesWithDesugarForSmartboxStaging'.

    com.android.build.api.transform.TransformException: java.lang.RuntimeException: java.lang.RuntimeException: com.android.ide.common.process.ProcessException: Error while executing java process with main class com.google.devtools.build.android.desugar.Desugar with arguments {--input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/20.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/22.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/44.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/46.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/19.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/21.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/38.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/40.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/5.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/7.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/54.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/56.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/32.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/34.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/3.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/5.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/37.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/39.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/8.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/10.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/51.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/53.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/60.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/62.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/6.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/8.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/18.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/20.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/42.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/44.jar --input /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/16.jar --output /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/desugar/smartbox/staging/18.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/classes/smartbox/staging --classpath_entry /Users/marisalopes/beneficiary-app/app/build/tmp/kotlin-classes/smartboxStaging --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/0.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/1.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/2.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/3.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/4.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/5.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/6.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/7.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/8.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/9.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/10.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/11.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/12.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/13.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/14.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/15.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/16.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/17.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/18.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/19.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/20.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/21.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/22.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/23.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/24.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/25.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/26.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/27.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/28.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/29.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/30.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/31.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/32.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/33.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/34.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/35.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/36.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/37.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/38.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/39.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/40.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/41.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/42.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/43.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/44.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/45.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/46.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/47.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/48.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/49.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/50.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/51.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/52.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/53.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/54.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/55.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/56.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/57.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/58.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/59.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/60.jar --classpath_entry /Users/marisalopes/beneficiary-app/app/build/intermediates/transforms/stackFramesFixer/smartbox/staging/61.jar --bootclasspath_entry /Users/marisalopes/Library/Android/sdk/platforms/android-27/android.jar --bootclasspath_entry /Users/marisalopes/Library/Android/sdk/platforms/android-27/optional/org.apache.http.legacy.jar --bootclasspath_entry /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/resources.jar --bootclasspath_entry /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/rt.jar --bootclasspath_entry /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/jsse.jar --bootclasspath_entry /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/jce.jar --bootclasspath_entry /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/charsets.jar --min_sdk_version 19 --nodesugar_try_with_resources_if_needed --desugar_try_with_resources_omit_runtime_classes --legacy_jacoco_fix}

    • Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

    • Get more help at https://help.gradle.org

    BUILD FAILED in 4s

    question 
    opened by MarisaLopes 4
  • store initialization

    store initialization

    Hi! I have been playing with this library for a while; let me tell you first of all that it looks great. I have always liked the Redux concept put in practice in web development, so thank you for bringing it out for Android :).

    My question has to do with the store initialization. Basically, I do not understand why you do,

    val mainStore = Store(
        reducer = ::counterReducer, 
        state = null
    )
    

    instead of,

    val mainStore = Store(
        reducer = ::counterReducer, 
        state = AppState()
    )
    

    I mean, according to the Redux concept, the app should always be in a particular state. I know the whole library is null-safety (?), but I have changed things as proposed and everything seems to work fine! Could you please shed some light on this matter? Regards,

    opened by pablodeafsapps 4
  • Reducer not being invoked

    Reducer not being invoked

    I have in MainApplication.kt (this extends Application()) the definition of the store (I'm following the ReduxMovieExample):

    val store = Store(
        reducer = ::appReducer,
        state = null,
        middleware = listOf(databaseMiddleware)
    )
    

    The appReducer is never invoked. It is defined in AppReducer.kt as:

    fun appReducer(action: Action, appState: AppState?) =
        AppState(
            passwordState = passwordReducer(action, appState?.passwordState)
        )
    

    My passwordReducer is not being called when I dispatch an action. My databaseMiddleware is being called to handle an action dispatched to it.

    UPDATE: I have looked at the source code to ReKotlin, specifically Store.kt. I am using

    store.dispatch(AddPassword(password))
    

    where AddPassword is an Action and is supposed to be handled by my passwordReducer function.

    In Store.kt, the function: override fun dispatch(action:Action) invokes this.dispatchFunction(action). As near as I can tell, dispatchFunction goes through the middleware list ONLY and never looks at the reducer. So if your middleware isn't handling the action, your reducer will never get it. Maybe I'm just used to Redux in React-Native, but this seems wrong to me. Or is there a different dispatch to use?

    opened by peterent 3
  • Enable multiplatform support

    Enable multiplatform support

    These changes will allow multiplatform use of the library. By switching to the common kotlin-stlib there are no JVM deps in the artifact. The jar can then be used in the common sourceSet of a MPP project.

    opened by patjackson52 3
  • Is this repository being updated regularly? Looking to contribute

    Is this repository being updated regularly? Looking to contribute

    Question above, I noticed this repository is significantly behind the ReSwift repo.

    opened by ZkHaider 2
  • Fix/27 concurrent modification

    Fix/27 concurrent modification

    see https://github.com/ReKotlin/ReKotlin/pull/29 for previous discussion

    opened by NemoOudeis 2
  • Middleware dispatches new action before old one updates state

    Middleware dispatches new action before old one updates state

    I have an issue where the state is updated out of order of the dispatched actions.

    In the flow outlined below, I would expect it to be called in the outlined order, with the end state being:

    isFetching: False
    isCompleted: True
    lastAction: AuthExists
    

    Due to some out-of-sync execution of stuff, the middleware gets called first, then the reducer, and the AuthCheck action overwrites the already-written AuthExists action, with an end result of such:

    isFetching: True
    isCompleted: True
    lastAction: AuthCheck
    

    Basically, instead of the flow getting executed as outlined below, it executes as: 1, 3, 4, 5, 2.

    1. Dispatch AuthCheck action on Activity onCreate
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_auth)
    
        [...]
    
        mainStore.subscribe(this) {
            it.select {
                it.userState
            }.skipRepeats { oldState, newState ->
                oldState == newState
            }
        }
    
        mainStore.dispatch(AuthCheck(null))
    }
    
    1. userReducer updates state
    fun userReducer(action: Action, state: UserState?) : UserState {
        val newState = state ?: UserState(AuthState.NotAuthed, "", "", "", "")
    
        when (action) {
            is AuthCheck -> {
                return newState.copy(isFetching = true, lastAction = action)
            }
    
            [...]
        }
        return newState
    }
    
    1. authMiddleware calls checkUser(action, dispatch)
    internal val authMiddleware: Middleware<AppState?> = { dispatch, _ ->
        { next ->
            { action ->
                when (action) {
                    is AuthCheck -> {
                        checkUser(action, dispatch)
                    }
    
                    [...]
    
                }
                next(action)
            }
        }
    }
    
    1. checkUser verifies for an existing user in Firebase, dispatching AuthExists
    private fun checkUser(action: AuthCheck, dispatch: DispatchFunction) {
        val auth = FirebaseAuth.getInstance()
        val user = auth.currentUser
    
        if (user != null) {
            dispatch(AuthExists(user.displayName!!, user.email!!, user.uid))
        } else {
            dispatch(AuthNotExists(null))
        }
    }
    
    1. userReducer updates state
    fun userReducer(action: Action, state: UserState?) : UserState {
        val newState = state ?: UserState(AuthState.NotAuthed, "", "", "", "")
    
        when (action) {
            
            [...]
    
            is AuthExists -> {
                return newState.copy(authState = AuthState.Authed, uId = action.uId, email = action.email,
                        name = action.name, isFetching = false, isCompleted = true, lastAction = action)
            }
    
            [...]
    
        }
    
        return newState
    }
    

    Can you please assist me on this one?

    opened by andreihava-okta 2
  • RecyclerView Adapter Init instead of notifyDataSetChanged()

    RecyclerView Adapter Init instead of notifyDataSetChanged()

    Hello, we have already integrated successfully ReSwift to our iOS app and now we are working on Android version. One thing that we noticed using ReKotlin in Android, is when the state has been changed we need to reassign the adapter to recyclerview in order to get the new data source.

     override fun newState(state: GlobalState) {
            
                state?.apply {
                    selectedList = state.selectedList
                    initiateAdapter()
                }
           
        }
    
    fun initiateAdapter() {
        adapter = AdapterClass(this, list, this)
    
        recView.adapter = adapter
        recView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
    }
    

    Why should we init adapter every time the data has been change instead of notifyDataSetChanged()?

    P.S. (we tried with notifyDataSetChanged() but the data didn't update)

    opened by billypap1 0
  • JCenter is being sunsetted

    JCenter is being sunsetted

    With the announcement of JCenter going away and delete warnings in android studio, please consider publishing to mavenCentral instead:

    https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/

    As of right now, removing jcenter() from build.gradle results in a compile time error:

    Could not find org.rekotlin:rekotlin:1.0.4.
         Searched in the following locations:
         ...
    
    opened by sjmueller 0
  • MOD: auto skip duplicate substate

    MOD: auto skip duplicate substate

    automaticallySkipRepeats should also affect substate

    opened by urakalee 0
  • ReKotlin Contravariant Store Subscriber

    ReKotlin Contravariant Store Subscriber

    Under the current setup we cannot create a StoreSubscriber for the supertype of our State. This is due to the fact that StoreSubscriber is a consumer of the StateType and without use of in declaration site-variance the type resolves to the concrete version of the StateType and will not allow it's super type to be a valid type.

    I wrote a test that verifies that this will work functionally. This doesn't test the actual change which would be caught at compile time because the removal of in would make the test class not legal.

    opened by erchenger 1
  • Suggestion

    Suggestion

    1.The async request in the Middleware cant be canceled when Activity is destroyed.Maybe we should add 'LifecycleMiddleware' with AndroidX LifecycleOwner? 2.The Global Store hold an AppState which has all the data of my activity even after my Activity is destroyed.Maybe we should add 'LifecycleStore' for single Activity, so the 'LifecycleStore' will be GCed when Activity is destroyed?

    opened by liyzay 0
  • BlockSubscriber - how to subscribe to sub-states correctly?

    BlockSubscriber - how to subscribe to sub-states correctly?

    Currently I have the following situation:

    AppState -> Sub-State1 (A) -> Sub-State2 (B)

    In my Fragment I used BlockSubscriber and have 2 subscriptions, subState1Subscription and subState2Subscription.

    I use it the following way:

    subState1Subscription= BlockSubscriber { state-> state?.let { handleState1(it) } }

    store.subscribe(subState1Subscription) { it.skipRepeats().select { state -> state.state1} }

    and:

    subState2Subscription= BlockSubscriber { state-> state?.let { handleState2(it) } }

    store.subscribe(subState2Subscription) { it.skipRepeats().select { state -> state.state2} }

    The problem I have is : everytime subState1Subscription triggers (state changes), then subState2Subscription is also triggered.

    Is this the right behavior or am I using BlockSubscriber wrong?

    opened by ramden 1
  • Cancel async action

    Cancel async action

    I am missing an obvious way to cancel pending async actions? In my app i am using action creators that spin off couroutines for handling network requests in the background. In some circumstances I want to be able to cancel the requests (e.g., when the user navigates to another activity before request has completed). My question is, what would be the best way to achieve this? In redux (thanks to thunk) action creators can return e.g., a Promise which can be used for cancelling https://stackoverflow.com/questions/38608417/how-to-to-cancel-pending-asynchronous-actions-in-react-redux and https://github.com/reduxjs/redux/issues/723#issuecomment-139927639. Another option might be to put the cancellables in the app state but then cancelling tasks feels like a side-effect see https://github.com/ReSwift/ReSwift/issues/214 I also saw a request for redux-thunk like middleware here https://github.com/ReKotlin/ReKotlin/issues/24. While the functional implementation of this thunk middleware looks straight forward, it does not allow to return a deferrable. Please provide option or strategy to cancel async actions

    opened by macsdragon 0
  • Improve Reducer with type-safe

    Improve Reducer with type-safe

    typealias Reducer<ReducerStateType> = (action: Action, state: ReducerStateType?) -> ReducerStateType

    is not type-safe because ReducerStateType can be anything.

    I suggest deprecate the current typealias Reducer and add the new interface Reducer below to make sure action and state are in the required types:

    interface Reducer<ReducerActionType : Action, ReducerStateType : StateType> {
        operator fun invoke(action: ReducerActionType?, state: ReducerStateType?): ReducerStateType
    }
    

    Usage example:

    interface CounterAction: Action
    
    object CounterActions {
        data class Up(val unit: Unit = Unit) : CounterAction
        data class Down(val unit: Unit = Unit) : CounterAction
        data class Reset(val unit: Unit = Unit) : CounterAction
    }
    
    object AppReducer : Reducer<Action, AppState> {
        override fun invoke(action: Action?, state: AppState?) = AppState(
            counterState = CounterReducer(action as? CounterAction, state?.counterState)
        )
    }
    
    object CounterReducer : Reducer<CounterAction, CounterState> {
        override fun invoke(action: CounterAction?, state: CounterState?) = (state ?: CounterState()).run {
            when (action) {
                is CounterActions.Up -> copy(counter = counter + 1)
                is CounterActions.Down -> copy(counter = counter - 1)
                is CounterActions.Reset -> CounterState()
                else -> this
            }
        }
    }
    
    opened by nguyenxndaidev 6
  • ReKotlin-Thunk interest?

    ReKotlin-Thunk interest?

    See: https://github.com/ReSwift/ReSwift-Thunk/ https://github.com/reduxjs/redux-thunk

    There's plenty of movement on this endeavor in the greater redux community, and I was wondering if the ReKotlin org would be infestered in taking it on, or for someone to?

    opened by jimisaacs 1
  • Caveat: same-state transitions on Activity finishing

    Caveat: same-state transitions on Activity finishing

    This does not actually relate to an issue, but to a fact I've figured out recently, regarding ReKotlin.

    I have an application which runs an Activity subscribed to a state. After a couple of operations (dispatching Action items), finish() is called, un-subscribing the Activity to the aforementioned state. Surprisingly, this same Activity kept on receiving updates and, after debugging for a while, I spotted that onStop() was not "fast" enough doing his job. My belief is that since all reducers get called on any state update (from any UI entity), the Activity was getting notified with the same information before it finished.

    After searching a bit on the examples proposed, I came across skipRepeats, which allows to dismiss updates when the state has not actually changed.

    Thus, now I have something like,

    override fun onStart() {
            super.onStart()
    
            store.subscribe(this) {
                it.select {
                    it.mainState
                }.skipRepeats { oldState, newState ->
                    oldState == newState
                }
            }
        }
    

    giving time enough to the Activity to invoke onStop and un-subscribing.

    If my reasoning is right, I suggest the author to include a brief explanation about this in the README file.

    enhancement 
    opened by pablodeafsapps 0
Releases(1.0.4)
Extendable MVI framework for Kotlin Multiplatform with powerful debugging tools (logging and time travel), inspired by Badoo MVICore library

Should you have any questions or ideas please welcome to the Slack channel: #mvikotlin Inspiration This project is inspired by Badoo MVICore library.

Arkadii Ivanov 614 Aug 1, 2021
MVU for Kotlin Multiplatform

Oolong Oolong is an Elm inspired Model-View-Update (MVU) implementation for Kotlin multiplatform. As the name implies, three core concepts comprise th

Oolong 259 Jul 30, 2021
Redux implementation for Kotlin (supports multiplatform JVM, native, JS, WASM)

Redux-Kotlin ![badge][badge-ios] A redux standard for Kotlin that supports multiplatform projects. Full documentation at http://reduxkotlin.org. Misso

Redux-Kotlin 186 Jul 27, 2021
IGameDealz - MVVM Example App

An App to find the cheapest deals for online games, IGameDealz searches 10 different online stores such as Steam and Uplay to find you the best deal for online games.

Daniel Butler 4 Jul 1, 2021
Model-View-ViewModel architecture components for mobile (android & ios) Kotlin Multiplatform development

Mobile Kotlin Model-View-ViewModel architecture components This is a Kotlin Multiplatform library that provides architecture components of Model-View-

IceRock Development 385 Jul 29, 2021
Kotlin Multiplatform Router for Android and iOS

A powerful Kotlin Multiplatform Router for Android and iOS Support I am happy to help you with any problem on gitter Feel free to open any new issue!

Sebastian Sellmair 336 Jun 24, 2021
BaseDemo 是Android MVVM + Retrofit + OkHttp + Coroutine 协程 + 组件化架构的Android应用开发规范化架构

BaseDemo 是Android MVVM + Retrofit + OkHttp + Coroutine 协程 + 组件化架构的Android应用开发规范化架构,通过不断的升级迭代,目前主要分为两个版本,分别为分支 MVVM+Databinding 组件化版本,分支MVVM+Databinding+Single 单体版本。旨在帮助您快速构建属于自己的APP项目架构,做到快速响应上手,另外再长期的实践经验中汇总了大量的使用工具类,主要放在了项目 `lib_common` 组件中,以供大家参考使用。具体使用请开发者工具自己项目需求决定选择如何使用。

Huan Zhou 35 Jul 22, 2021
A sample project in Kotlin to demonstrate AndroidX, MVVM, Coroutines, Hilt, Room, Data Binding, View Binding, Retrofit, Moshi, Leak Canary and Repository pattern.

This repository contains a sample project in Kotlin to demonstrate AndroidX, MVVM, Coroutines, Hilt, Room, Data Binding, View Binding, Retrofit, Moshi, Leak Canary and Repository pattern

Areg Petrosyan 5 May 24, 2021
Moxy is MVP library for Android

Moxy This Moxy repository is deprecated and no longer supported. Please migrate to the actual version of the Moxy framework at Moxy communuty repo. De

Arello Mobile 1.6k Jul 25, 2021
A full-featured framework that allows building android applications following the principles of Clean Architecture.

EasyMVP A powerful, and very simple MVP library with annotation processing and bytecode weaving. EasyMVP eliminates the boilerplate code for dealing w

null 1.3k Jul 21, 2021
以QMUI+Jetpack组件封装的一个MVVM基础框架

以QMUI+Jetpack组件封装的一个MVVM基础框架。 主要提供了BaseQMUIFragment、BaseVmFragment和BaseVmDbFragment三个基类和QMUI一些组件的扩展封装。

null 0 Jul 22, 2021
A data-binding Presentation Model(MVVM) framework for the Android platform.

PLEASE NOTE, THIS PROJECT IS NO LONGER BEING MAINTAINED. As personal time contraints, I am currently unable to keep up. Please use official android da

RoboBinding open source 1.3k Jul 14, 2021
MVVM RECIPE ANDROID APP Is an app where I show how to use MVVM, retrofit, dagger hilt, coroutine, liveData, Kotlin, navigation component, and so on...

MVVM RECIPE ANDROID APP Is an app where I show how to use MVVM, retrofit, dagger hilt, coroutine, liveData, kotlin, navigation component, and so on...

Isaias Cuvula 6 Jul 7, 2021
Membuat Aplikasi Github User MVVM dengan Android Studio

Membuat Aplikasi Github User MVVM dengan Android Studio. Ini hanya untuk referensi bagi kalian yang mengikuti Submission Dicoding Github User App.

Azhar Rivaldi 4 Jul 5, 2021