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

Related tags

MVVM/MVP 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

Comments
  • 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
  • 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
  • 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
  • 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
  • update for multiplatform support and include Android & iOS sample

    update for multiplatform support and include Android & iOS sample

    Major restructure of the project to support multiplatform projects. The project now uses the kotlin-multiplatform gradle plugin to build and publish artifacts to maven. Gradle metadata is enabled, so multiple artifacts are published (one for each platform), and they can be pulled into the app using the lib with one line ( `implementation 'org.rekotlin:rekotlin:1.1.0')

    Also included is a sample Android & iOS app. This will help with testing the library and others getting started with multiplatform.

    The redux code itself is unchanged.

    opened by patjackson52 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
  • view by lazy

    view by lazy

    Hello. This

        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
        }
    

    produces three anonymous classes for lambdas and three extra objects for properties in the bytecode, and creates three synchronized lazies. Consider using Kotlin Android Extensions instead.

    opened by Miha-x64 1
  • 'dispatch()' vs 'dispatch.invoke()'

    'dispatch()' vs 'dispatch.invoke()'

    Straight to the point, as the title says. I've seen a bunch of examples using the first form in Activity/Fragment instances, and the latter in Middleware objects. Is there any difference between them? When shall I use each? Regards!

    question 
    opened by pablodeafsapps 1
  • 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 3
  • 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
Releases(1.0.4)
Owner
null
A simple app that consumes The GitHub APIs to display github users. The aim was to learn about different jetpack libraries, built with MVVM pattern.

A simple app that consumes The GitHub APIs to display github users. The aim was to learn about different jetpack libraries, built with MVVM pattern.

Odhiambo Brandy 4 Apr 15, 2022
Kotlin+Flow+Retrofit+OKHttp+ViewBanding+ViewModel+LiveData封装的一个Kotlin版本的MVVM框架

Gitee 地址:kmvvm Github 地址:kmvvm CHANGE LOG 技术要点 支持Flow+Retrofit+OkHttp实现链式http请求 支持Rxjava+Retrofit+OkHttp实现链式http请求 封装基类:BaseActivity、BaseVMActivity、Ba

抓猪 76 Nov 21, 2022
App kotlin with flow, paging 3, MVVM, Room, Dagger hilt

TMDBTest App kotlin with flow, paging 3, MVVM, Room, Dagger hilt Para compilar la app se tiene que descargar el proyecto. Luego poner la APIKEY de TMD

null 3 Sep 9, 2022
Android MVVM with Single Activity sample app that uses kotlin coroutines flow

Android MVVM with Single Activity sample app that uses kotlin coroutines flow. This is a sample app that uses kotlin coroutines flow , stateflow. This

null 4 Jul 15, 2022
AAC MVVM + Clean Architecture + Coroutine Flow

GithubBrowser MVVM Clean Architecture Sample AAC MVVM + Clean Architecture + Coroutine Flow 본 샘플 프로젝트는 https://github.com/omjoonkim/GitHubBrowserApp 을

Jeonguk-JayDev 18 May 25, 2022
ComposeKit with MVVM, DI, Coroutines,Flow

ComposeKit ComposeKit with MVVM, DI, Coroutines,Flow ComposeKit Kotlin Multiplatform project using Jetpack Compose and SwiftUI Note: Intital implement

Anuj Sachan 8 Oct 13, 2022
MVVM ,Hilt DI ,LiveData ,Flow ,Room ,Retrofit ,Coroutine , Navigation Component ,DataStore ,DataBinding , ViewBinding, Coil

MVVM ,Hilt DI ,LiveData ,Flow ,Room ,Retrofit ,Coroutine , Navigation Component ,DataStore ,DataBinding , ViewBinding, Coil

Ali Assalem 12 Nov 1, 2022
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 8 Nov 4, 2022
Pick any of your favorite github repository and create a mini android app showing its details on an android app.

Github Browser Pick any of your favorite github repository and create a mini android app showing its details on an android app. Screens navigation gra

Prasoon 6 Oct 16, 2022
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 39 Nov 5, 2022
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 Nov 15, 2022
🧬 Android DataBinding kit for notifying data changes from Model layers to UI layers on MVVM architecture.

?? Android DataBinding kit for notifying data changes from Model layers to UI layers on MVVM architecture.

Jaewoong Eum 274 Oct 21, 2022
Retrieve Data from an API using MVVM Clean Architecture and Jetpack Compose

MVVM Clean Architecture Demo Retrieve Data from an API using MVVM Clean Architecture and Jetpack Compose. It simply shows a list of movies fetched fro

Daniel Kago 2 Sep 16, 2022
🍿 A TV Showcase App using Jetpack libs and MVVM arch. Data provided by TV Maze API

TV Showcase A TV Showcase ?? Android App using Jetpack libraries and MVVM architecture. Data provided by TVMaze API. Release ?? Download here See how

Lucas Rafagnin 4 Sep 8, 2022
Discover the most popular and top rated movies playing. Movies data fetched using tomdbapi.com API.

Movie-App A simple news app build using MVVM architecture. Discover the most popular and top rated movies playing. Movies data fetched using tomdbapi.

Mahmudul Hasan 4 Nov 24, 2022
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 22 Oct 26, 2022
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 343 Nov 16, 2022
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 288 Nov 29, 2022