Android + Redux = <3

Hey! My name is Vitaly Sulimov, I am an Android developer at Wheely, and today I would like to talk to you about the architecture of mobile applications. Namely, how we at the company applied the Redux architecture to our two applications and what came of it.





Disclaimer # 1





Android- 2016- , MVC, MVP, Moxy Arello Mobile, Clean Architecture Redux. — . , , . , , , . — Android-.





?

Redux, , :





1. View





View — , -, , , — (, , , , Pull To Refresh, ..)





2. , - /





, . 





— , runtime, , , .





3.





, , . , / ..





4.





, , , - . , , : , . 





Redux, !

Redux Android-, , , Android.





Redux . , Store, , , - , , - . , , Redux.





State





, Redux. () . 





Store





Store (State) , Middleware Reducer





API , (Action), .





Action





, , Store. ( ). .





Reducer





, (Action). , , () ( , , , ..).





, Reducer Copy-on-write , .





Middleware





Middleware , (Action) , , Reducer.





Middleware , - . 





Redux Android?

. Android (Action), , , Activity, View, BroadcastReceiver - Action , ( ).





Talk is cheap. Show me the code.





, , Redux , . Counter, , . . .





?





Android Studio Redux Kotlin.





GitLab.





Redux, Android. , , , !





#2





, Rx, Coroutines, - , , . KISS, , .









, Redux? , Copy-on-write. , Kotlin - data class. 





ApplicationState.kt





data class ApplicationState(
    val counter: Int = 0
)
      
      



, , .









Action Redux. Action . Middleware Reducer, sealed class’, , . .





CounterAction.kt





sealed class CounterAction : Action {
  
    object Increment : CounterAction()
    
    object Reset : CounterAction()
}
      
      



Reducer





, Reducer<S>, S - , .. ApplicationState. - - reduce. , .





CounterReducer.kt





object CounterReducer : Reducer<ApplicationState> {
  
    override fun reduce(action: Action, state: ApplicationState): ApplicationState =
        when (action) {
            is CounterAction.Increment ->
                state.copy(counter = state.counter.inc())
                
            is CounterAction.Reset ->
                state.copy(counter = 0)
                
            else ->
                state
        }
}
      
      



Store





, Store





Store . - AbstractStore<S> . Middleware Reducer.





ApplicationStore.kt





class ApplicationStore(
    initialState: ApplicationState,
    middlewares: List<Middleware<ApplicationState>>,
    reducers: List<Reducer<ApplicationState>>
) : AbstractStore<ApplicationState>(initialState, middlewares, reducers)
      
      



ApplicationStore, Middleware Reducer. Store, ApplicationState - AppComponent Store .





AppComponent.kt





object AppComponent {
    val store = ApplicationStore(
        initialState = ApplicationState(),
        middlewares = emptyList(),
        reducers = listOf(CounterReducer)
    )
}
      
      



, , .





ReduxFunctions.kt





fun dispatch(action: Action) =
    AppComponent.store.dispatch(action)
    
fun subscribe(subscription: Subscription<ApplicationState>) =
    AppComponent.store.subscribe(subscription)
    
fun unsubscribe(subscription: Subscription<ApplicationState>) =
    AppComponent.store.unsubscribe(subscription)
      
      







, , , , reducer, , . , . , reducer’a, , store . , UI!





Android-. (Single Activity / Multiple Activities / Fragments?), , - Activity View. Activity View, .





CounterView.kt





class CounterView(
    context: Context
) : FrameLayout(context) {
  
    private val counterSubscription = SubStateSubscription<ApplicationState, Int>(
        transform = { it.counter },
        onStateChange = { state: Int, _: Boolean -> handleCounterStateChange(state) }
    )
    
    private lateinit var counterTextView: TextView
    private lateinit var floatingActionButton: FloatingActionButton
  
    init {
        inflate(context, R.layout.view_counter, this)
        findViewsById()
        setOnClickListeners()
    }
    
    private fun findViewsById() {
        counterTextView = findViewById(R.id.counterTextView)
        floatingActionButton = findViewById(R.id.floatingActionButton)
    }
    
    private fun setOnClickListeners() {
        floatingActionButton.setOnClickListener { dispatch(CounterAction.Increment) }
    }
    
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        subscribeToStateChanges()
    }
    
    private fun subscribeToStateChanges() {
        subscribe(counterSubscription)
    }
    
    override fun onDetachedFromWindow() {
        unsubscribeFromStateChanges()
        super.onDetachedFromWindow()
    }
    
    private fun unsubscribeFromStateChanges() {
        unsubscribe(counterSubscription)
    }
    
    private fun handleCounterStateChange(state: Int) {
        counterTextView.text = state.toString()
    }
}
      
      



, . SubStateSubscription, , , - , , Rx, map(), - .





, lateinit var View. 





. XML-, Floating Action Button. dispatch CounterAction.Increment, . 





OnViewAttached / Detached from window.





, , . , TextView.





CounterView.kt





...
counterTextView.text = state.toString()
...
      
      



! .





, , View , counter ApplicationState, , by design, Application View , … , , , ( “”). ? .





, Redux Android

, Android Redux. . AppCompatActivity, AppCompatActivity : Activity ActivityLifecycleAction ( ) . - AppCompatActivity Store, . .





MainActivity.kt





class MainActivity : AppCompatActivity<ApplicationState>() {
  
    private lateinit var contentViewGroup: ViewGroup
  
    override fun getStore(): Store<ApplicationState> =
        AppComponent.store
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewsById()
        addCounterView()
    }
    
    private fun findViewsById() {
        contentViewGroup = findViewById(R.id.contentViewGroup)
    }
    
    private fun addCounterView() {
        contentViewGroup.addView(CounterView(context = this))
    }
}
      
      



Middleware





- MIddleware. , : Activity (onDestroy) isFInishing == true - . 





isFinishing , true, , , false - , . 





, , . , Middleware<S>, S - handleAction().





ActivityLifecycleMiddleware.kt





object ActivityLifecycleMiddleware : Middleware<ApplicationState> {
  
    override fun handleAction(
        action: Action,
        state: ApplicationState,
        next: Next<ApplicationState>
    ): Action {
        val newAction = when (action) {
            is ActivityLifecycleAction.OnDestroy ->
                handleActivityOnDestroy(action)
                
            else ->
                action
        }
        return next(newAction, state)
    }
    
    private fun handleActivityOnDestroy(action: ActivityLifecycleAction.OnDestroy): Action =
        if (action.isFinishing) CounterAction.Reset else action
}
      
      



, . ActivityLifecycleAction.OnDestroy Reducer, Middleware, , . , isFinishing == true, Reducer CounterAction.Reset, , false - , , , . middleware AppComponent-.





AppComponent.kt





store = ApplicationStore(
    initialState = ApplicationState(),
    middlewares = listOf(ActivityLifecycleMiddleware),
    reducers = listOf(CounterReducer)
)
      
      



!





Redux. , , — . , JavaScript. , , , . . - - Action. - Middleware, - Reducer. View , .









, , Counter, Middleware Reducer , . Redux- — “”, Android ( ), API OpenWeatherMap. .





https://gitlab.com/v.sulimov/android-redux-kotlin





https://gitlab.com/v.sulimov/android-redux-demo





https://gitlab.com/v.sulimov/android-openweather-kotlin









, Redux Android, . , , , , Redux , , . , - .



, , .





, . Wheely.








All Articles