Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Clean architecture with redux in Android by Cat...

Clean architecture with redux in Android by Catalin Comarnescu

Clean architecture with redux in android talk given by Catalin Comarnescu at the android munich meetup.

Benjamin Stürmer

September 04, 2019
Tweet

Other Decks in Programming

Transcript

  1. In the beginning the Universe was created. This has made

    a lot of people very angry and been widely regarded as a bad move.
  2. 2017 Westwing Android App Status Quo • No clear architecture

    (some MVP, some DI, some MVVM) • High coupling (context everywhere) • Lots of hidden business logic in the app, hard to refactor • API-derived entities used almost directly in the views • Some unit + API tests, but difficult to create for old code • Extensive end-to-end tests • Quite stable, no business case for refactoring
  3. 2018 WestwingNow App Goals • Standardized architecture • Decoupled •

    Easy to refactor • Easy to make changes to the UI • Unit testable, including view state • Isolate business logic • Isolate API-derived entities
  4. Clean Architecture? • Independent of frameworks • Testable • Independent

    of UI • Independent of database • Independent of any external agency
  5. Dependency Rule • Entities are independent • Use cases know

    entities • Adapters know use cases • Frameworks know adapters
  6. AddToCart UseCase Product Details Activity ViewModel Cart Repository API Retrofit

    In-Memory Cache Dependencies app (presentation) domain data
  7. AddToCart UseCase Product Details Activity ViewModel Cart Repository domain API

    Retrofit Call API sku Action AddToCart View State addCartProgress Execute UseCase sku Call Repository sku Observable AddToCartResponse Observable AddToCartStatus Observable AddToCartStatus View State addCartSuccess maxQuantity - 1 app (presentation) data
  8. Testing • Unit tests - JUnit • API tests -

    Retrofit • Integration tests - Robolectric • Component tests - Robolectric • Visual regression - Espresso • UI tests - Espresso, UIAutomator
  9. View state management complexity Async processes • Load product •

    Load color variants • Load related products User interactions • Select color variant • Select related product • Add to cart, wishlist, sign up for reminder
  10. Redux fundamental principles • Single source of truth for UI

    state • State is read-only • Mutations are written as pure functions
  11. State data class ProductViewState( val name: String, val sku: String,

    val price: String, val image: Image, val cartButtonState: CartButtonState, val maxQuantity: Int )
  12. State data class ProductViewState( val name: String, val sku: String,

    val price: String, val image: Image, val cartButtonState: CartButtonState, val maxQuantity: Int ) enum class CartButtonState { ENABLED, DISABLED, PROCESSING }
  13. Dispatch Action data class AddToCart(val quantity: Int) : Action() //

    button.setOnClickListener { ... } dispatcher.dispatch(AddToCart(1))
  14. Reduce State for Action fun reduce(oldState: State, action: Action): State

    { return when (action) { is AddToCart -> oldState.copy(cartButtonState = PROCESSING)) else -> oldState } }
  15. Reduce State for Action fun reduce(oldState: State, action: Action): State

    { return when (action) { is AddToCart -> oldState.copy(cartButtonState = PROCESSING)) else -> oldState } } store.state = reducer.reduce(store.state, ChangeQuantity(2))
  16. Reduce State for Action fun reduce(oldState: State, action: Action): State

    { return when (action) { is AddToCart -> oldState.copy(cartButtonState = PROCESSING)) else -> oldState } } store.state = reducer.reduce(store.state, ChangeQuantity(2)) // In activity or fragment store.doOnStateChanged { // update view elements }
  17. Refactor: reduce each View State element fun reduce(oldState: State, action:

    Action): State { return ProductViewState( name = reduceName(oldState.name, action), … cartButtonState = reduceCartButton(oldState.maxQuantity, action), maxQuantity = reduceMaxQuantity(oldState.maxQuantity, action) ) }
  18. Refactor: reduce each View State element fun reduce(oldState: State, action:

    Action): State { return ProductViewState( ... cartButtonState = reduceCartButton(oldState.maxQuantity, action), ... ) } private fun reduceCartButton(oldCartButtonState: CartButtonState, action: Action): CartButtonState { return when (action) { is AddToCart -> PROCESSING else -> oldCartButtonState } }
  19. Refactor: reduce each View State element fun reduce(oldState: State, action:

    Action): State { return ProductViewState( ... maxQuantity = reduceMaxQuantity(oldState.maxQuantity, action) ... ) } private fun reduceMaxQuantity(oldQuantity: Int, action: Action): Int { return when (action) { is AddToCart -> (oldQuantity - action.quantity) else -> oldQuantity } }
  20. View Dispatcher Reducer Side Effect Store Use Case ui presentation

    domain State State Action State, Action State, Action Action
  21. Dispatch Side Effects store.state = reducer.reduce(store.state, ChangeQuantity(2)) private fun dispatchSideEffects(oldState:

    State, action: Action) { if (action is AddToCart) { addToCartUseCase(oldState.sku, action.quantity) .execute() .observe( { success -> dispatcher.dispatch(AddToCartDoneAction)) }, { error -> dispatcher.dispatch(AddToCartErrorAction(error))) } ) } }
  22. Reduce State for Action private fun reduceAddToCartButton( oldCartButtonState: CartButtonState, action:

    Action): CartButtonState { return when (action) { is AddToCart -> PROCESSING is AddToCartDone, is AddToCartError -> ENABLED else -> oldCartButtonState } }
  23. View Dispatcher Reducer Side Effect Store Use Case ui presentation

    domain State State Action State, Action State, Action Action
  24. Testing @Test fun `After adding to cart an expected view

    state is emitted`() { `when`(addToCartUseCase.execute("sku", 1))).thenReturn(Completable.complete()) dispatcher.dispatch(AddToCart("sku", 1)) testObserver.verifyStates( emptyViewState, // initial viewstate expectedStateAddToCartInprogress, // adding to cart in progress state expectedStateAddToCartSucceeded, // adding to cart succeeded state ) }