Skip to content

JekaK/Redux-MVI-for-Android

Repository files navigation

Redux-MVI-for-Android

Release codebeat badge GitHub license

This repository provides a Redux-MVI architecture implementation for Android applications. It is designed to help developers build scalable, maintainable, and testable Android applications.

Installation

To install the library in your Android project, follow these steps:

  1. Add the JitPack repository to your build file. To do this, add the following lines to your build.gradle file:

    allprojects {
        repositories {
            maven { url 'https://jitpack.io' }
        }
    }
  2. Add the dependency to your app's build.gradle file:

    dependencies {
         implementation 'com.github.JekaK:Redux-MVI-for-Android:version'
    }
    

Note that you should replace version with the latest release version available on JitPack.

Example

  1. Start a Koin DI(it's powered by Koin, so you don't have a lot of choise what DI use). viewModelModule is module provided by sample. You will create it by yourself. Or not if you not using ViewModels at your project. Also setup a ViewState, because a default one is using Any() type. Other modules provided by library and SHOULD BE CONNECTED AS BELOW:
 class App : Application() { 
  
     override fun onCreate() { 
         super.onCreate() 
         setupDIGraph() 
     } 
  
     private fun setupDIGraph() { 
         startKoin { 
             androidContext(this@App) 
             modules( *listOfModules, *viewModelModule) 
         }
      val store: Store<Action, AppState> = get()
      store.dispatch(SetupStateAction(ViewState()))
     }
 } 
  1. Then I recomend you to create a package called "Presentation" and put 3 files there: State, Props and ViewModel.

State class is for saving state of app. Props is mapped class as in Clean Arch and it created for state that will be used only in views and not the whole one project state. ViewModel is for communicating between view and state updates.

State is DataClass:

data class MainState( 
     val isOpen: Boolean = false, 
     val counter: Int = 0 
 ) 

Props also a DataClass. And in my case it contains a mapper to(cause it's tiny and not deserve a separate class :D):

 data class MainProps( 
     val counter: Int = 0, 
     val addCounterAction: () -> Unit = {} 
 ) 
  
 fun MainState.toProps(addCounterAction: () -> Unit): MainProps { 
     return MainProps(this.counter) { 
         addCounterAction() 
     } 
 } 

ViewModel contains a store from Redux architecture for dispatching actions and have a link to state for view usage:

 class MainViewModel( 
     private val store: Store<Action, AppState>, 
     private val bindingDispatcher: CoroutineDispatcher 
 ) : ViewModel() { 

In init block I adding a state for this screen to state list by pass it to dispatched action. This give us ability to save state and retrieve it from Flow.

 init { 
     store.dispatch(AddStateAction(MainState())) 
 } 

Also mainProps give us a Flow of props that contains state mapped to props and can be used in our Activity or Fragment or Composable function navigated by Composable navigation.

fun mainProps(): Flow<MainProps> {
   return store.stateFlow()
      .takeWhenChangedAsViewState<ViewState, MainState> {
         it.mainState
      }
      .map { mainState ->
         MainProps(mainState.counter, this::addCounter)
      }
      .flowOn(bindingDispatcher)
}
  1. Create an action for some busines logic. In our case we have a button with counter. When we click on button action is dispatching and redusing a new state. This will trigger a state update via flow and show result to user:
class AddCounterAction : ReducibleAction {
   override fun reduce(state: AppState): AppState {
      return state.copy(
         viewState = state.viewState.updateViewState<ViewState> {
            it.copy(
               mainState = it.mainState.copy(
                  counter = it.mainState.counter + 1
               )
            )
         }
      )
   }
}
  1. Then use it in Acivity as collectAsState function:
val props by viewModel.mainProps().collectAsState(initial = MainProps())
Column(
   modifier = Modifier.fillMaxSize(),
   horizontalAlignment = Alignment.CenterHorizontally,
   verticalArrangement = Arrangement.Center
) {
   CounterView(props.counter)
   Spacer(modifier = Modifier.height(20.dp))
   AddButton(viewModel::addCounter)
}

Middleware

The Redux-MVI-for-Android package provides middleware as a way to intercept and modify actions and state changes in the Redux store. One example of middleware provided in this package is the "MainMiddleware", which send a message action via navigation requests when it intercepted by it.

class MainMiddleware : Middleware<Action, Store<Action, AppState>> {

    /**
     * > When the `AddCounterAction` is dispatched, dispatch a `CounterToastAction`
     *
     * The `dispatchCounterToastAction` function is defined as follows:
     */
    override fun dispatch(
        action: Action,
        store: Store<Action, AppState>,
        next: Dispatcher<Action, Store<Action, AppState>>,
    ): Action {
        // You can make a dispatch right in return, but if you want not updated state and somehow modify it before
        // action is will be dispatched to next middleware - make it as in example below
        val next = next.dispatch(action, store)

        when (action) {
            is AddCounterAction -> dispatchCounterToastAction(store)
        }

        return next
    }


    /**
     * "Dispatch a toast action that shows the current counter value."
     *
     * The function is private because it's only used internally by the MainActivity
     */
    private fun dispatchCounterToastAction(store: Store<Action, AppState>) {
       val counter = store.getState().viewState.asViewState<ViewState>().mainState.counter

       store.dispatch(ShowCounterToastAction(counter))
    }

}

For middleware setup you should provide your own MiddlewareModule as in Sample package and connect it to Koin in App class.

!!!IMPORTANT!!!

For better performance you can create a custom feature for setup only this middlewares that you will use in particular screen.

Example: you have a project with 100+ middlewares that potentially can handle all actions. They all will be added to a chain of middlewares. But in different screens you need only few middlewares in one, so you can add a Feature module and setup a middleware as Feature fo some screen by emiiting (SetupFeature(Feature) in init block of ViewModel and CleanupFeature(Feature) in onCleared() function of ViewModel. For additional info check MainViewModel at Presentation package, FeatureModle and MiddlewareModule at DI package. All in sample package as well.

License

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