Skip to content

rozag/kozy-redux

Repository files navigation

kozy-redux-core kozy-redux-base Tweet

Pure Kotlin redux library for Android. This library is being developed with several key ideas:

  • It should be tiny
  • The code should be as simple as possible
  • It should be relatively easy to adapt the library to your use case. Don’t hesitate to file an issue if you hit any blocker

Table of contents

Quick start

Add the dependency to your application module's build.gradle.

dependencies {
    implementation "com.github.rozag:kozy-redux-base:0.5"
}

Define a state class. It will keep the entire state of your app. Normally you don't mutate your state, so make it immutable. It is a good practice to define an INITIAL state.

data class MyState(val number: Int) : ReduxState {
    companion object {
        val INITIAL: MyState = MyState(number = 0)
    }
}

Define an action sealed class. Sealed classes make routing actions to reducers a breeze because the else branch in the when expression can be dropped (with sealed classes the Kotlin compiler can prove that all cases have been handled) - see the root reducer sample below for details.

sealed class MyAction : ReduxAction {
    class SetUp : MyAction()
    class TearDown : MyAction()
    sealed class Feed : MyAction() {
        // Feed actions classes go here
    }
    sealed class Profile : MyAction() {
        // Profile actions classes go here
    }
}

Define a root reducer function. Reducer is a pure function that takes the previous state and an action and returns a new state. Your root reducer is like a router that routes different actions and different state parts to different reducers. Kotlin's when expression is your friend here. Note that your child reducers can accept a tiny piece of the state tree - they don't usually need the whole app state. The SetUp action in the snippet below is used to inflate your initial state. In this simple tutorial we don't need it, but you can use this pattern in your apps. Same with the TearDown action - we don't want links to our objects after the app is closed. You can use those actions to populate unneeded state fields with default values.

fun rootReducer(state: MyState, action: MyAction): MyState = when (action) {
    is MyAction.SetUp -> MyState.INITIAL
    is MyAction.TearDown -> MyState.INITIAL
    is MyAction.Feed -> MyState(feedReducer(state.number, action))
    is MyAction.Profile -> MyState(profileReducer(state.number, action))
}
fun feedReducer(number: Int, action: MyAction.Feed): Int { ... }
fun profileReducer(number: Int, action: MyAction.Profile): Int { ... }

Create a store object. You can place it wherever you want - inside your Application class, for instance. You can also provide your store to other components via any DI framework. But remember: there should be only one instance of the store in your app.

typealias MyStore = ReduxSubscribableStore<MyState, MyAction>
class MyApplication : Application() {
    companion object {
        val Store: MyStore = SubscribableStore(MyState.INITIAL, ::rootReducer)
        /* 
         * You can also use the 
         *                      SubscribableBufferedStore(
         *                              MyState.INITIAL, 
         *                              ::rootReducer, 
         *                              MY_BUFFER_SIZE_LIMIT,
         *                              MY_INITIAL_BUFFER_SIZE
         *                      ) 
         * constructor to create a state buffer backed store for the time travel stuff
         */
    }
    override fun onCreate() {
        super.onCreate()
        store.dispatch(CounterAction.SetUp())
    }
}

And now you're ready to go. Dispatch your actions to the store via store.dispatch(...) - your reducer will handle the action and return a new state. Subscribe to state updates in your classes via store.subscribe(...). The returned Subscription object allows you to unsubscribe from state updates. In Activity you can do it like this:

class MyActivity : AppCompatActivity(), ReduxSubscribableStore.Subscriber<MyState> {
    private val store: MyStore = MyApplication.Store
    private lateinit var subscription: ReduxSubscribableStore.Subscription
    override fun onStart() {
        super.onStart()
        subscription = store.subscribe(this)
    }
    override fun onStop() {
        super.onStop()
        subscription.cancel()
    }
    override fun onNewState(state: MyState) {
        // Handle the new state
    }
}

Middleware

If you want to react to an action dispatch (logging, analytics, etc.) you can use middleware. In kozy-redux it is implemented as an abstract ReduxMiddleware class. Under the hood it simply wraps the store.dispatch(...) method. To add a new middleware to your app you should extend the ReduxMiddleware class, implement doBeforeDispatch(store, action) and doAfterDispatch(store, action) methods and apply your middleware to your store via the store.applyMiddleware(vararg middlewareList) method. For example, you can implement a middleware which will log every action and every new store state like this:

class LoggingMiddleware : ReduxMiddleware<ReduxState, ReduxAction, ReduxStore<ReduxState, ReduxAction>>() {
    override fun doBeforeDispatch(store: ReduxStore<ReduxState, ReduxAction>, action: ReduxAction) {
        Log.d("LoggingMiddleware", "Dispatching action: $action")
    }
 
    override fun doAfterDispatch(store: ReduxStore<ReduxState, ReduxAction>, action: ReduxAction) {
        Log.d("LoggingMiddleware", "New state: ${store.getState()}")
    }
}
// And apply it to your store
store.applyMiddleware(LoggingMiddleware())

Small tip: if you want to build the analytics middleware, it would be great to create your action hierarchy in such a way that you don't need any analytics.sendEvent(...) statements anywhere except your analytics middleware.

Action creators - handling async stuff

One interesting question is "Where should I put the asynchronous code?". The whole idea of reducers is that they should be pure functions - functions without any side effects or dependencies. The answer is action creators. Action creator is a function or a class that can create and dispatch actions to the store. kozy-redux doesn't provide any classes or interfaces for such entities - you're free to create them the way you like.

Let's say we want to perform a database operation and show a progress bar while the operation is running. The common way to handle such case is to create an action for this:

sealed class WriteSmthToDb : MyAction() {
    class Started : WriteSmthToDb()
    data class Success(val result: Result) : WriteSmthToDb()
    data class Error(val error: Error) : WriteSmthToDb
}

Now we want to perform an operation. First of all, you invoke your action creator (let it be WriteSmthToDbActionCreator). The action creator dispatches the WriteSmthToDb.Started action to the store and starts the async operation. Your reducer returns a new state. Your subscriber view receives the state with the flag that the progress bar should be visible and updates UI. When the operation finishes your action creator dispatches either WriteSmthToDb.Success or WriteSmthToDb.Error action to the store and your UI updates according to the new state.

class WriteSmthToDbActionCreator(val store: MyStore, val db: MyDatabase) {
    fun createAndDispatch(someData: String) {
        // The progress bar should be shown after dispatching this action
        store.dispatch(MyAction.WriteSmthToDb.Started())

        // This method is async, one of callbacks will be invoked later
        db.writeSmth(
                someData,
                { result -> store.dispatch(MyAction.WriteSmthToDb.Success(result)) },
                { error -> store.dispatch(MyAction.WriteSmthToDb.Error(error)) }
        )
    }
}

Buffered store

In some apps we need an undo-like functionality. Buffered store is a way to handle this kind of tasks. In kozy-redux buffered store looks like a ReduxBufferedStore interface and an implementation for it - a SubscribableBufferedStore class. The interface looks as following:

interface ReduxBufferedStore<S : ReduxState, A : ReduxAction> : ReduxStore<S, A> {
    fun bufferSizeLimit(): Int
    fun changeSizeLimit(newSizeLimit: Int)
    fun currentBufferSize(): Int
    fun currentBufferPosition(): Int
    fun resetBuffer(initialState: S)
    fun buffer(): List<ReduxState>
    fun jumpToState(position: Int)
    fun resetToState(position: Int)
}

Some of these methods are useful for testing, debugging or building a log when an error occurs. Let's take a look at the primary ones:

  • resetBuffer(initialState: S) clears the buffer, puts the initialState into it and notifies the subscribers. You can use this method with the TearDown action pattern, for example
  • jumpToState(position: Int) changes the state buffer pointer position to the specified index and notifies the subscribers
  • resetToState(position: Int) - same as jumpToState but the part of the buffer with index > position is removed

What's inside

Dependency Description
kozy-redux-core Core interfaces and implementations of store and buffered store (without the subscriptions stuff). Usually you don't use this dependency, however it's useful for future library development.
kozy-redux-base This one is built on top of the kozy-redux-core and provides a subscribable store interface and 2 store implementations: a subscribable store and a buffered subscribable store.

Samples

Sample Description
sample-counter Simple counter app: press operation button - see the result on the screen. Kind of hello world in a Redux world.
sample-counter-buffered Same as the sample-counter, but now the buffered store is used - time travel within your state buffer with a slider.

TODO

  • Add more complex sample project
  • Add best practices and tips to the wiki
  • kozy-redux-rx - store and buffered store with RxJava subscriptions

Additional reading

Contributing

If you find a bug - create an issue. It's your contribution. And PRs are always welcome.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgements

  • Redux - Predictable state container for JavaScript apps.