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

Separate your concerns with clean MVI

Separate your concerns with clean MVI

It’s become a norm to architect our code bases with either of the most popular architecture patterns. It’s even become a thing to have a hard time making decision on which pattern to pick given a number of options are available out there nowadays.

MVI is one of the latest to join the MV* gang in android Community. It best solves the state management problem by making the data/business logic drives the app’s state and leveraging data immutability. We’ll dive into the nitty-gritty of managing states the MVI way.

Views in android are already complex in its nature. MVI makes the view as dump and passive as possible to the point where the only concern left for a view is rendering data and firing events. Nothing more.

We’ll also go through a technique for building complex UI using a single recycler view with the help of Epoxy library which works very well with MVI. And of course, some caveats of using this library along with MVI.

Not necessarily a requirement but MVI shines the brightest when used together with reactive framework like Rxjava. Here’s the thing, simply using Rxjava doesn’t make your app reactive. You might have heard or seen something similar to this a couple of times, I’m using Rxjava/LiveData but still have to refresh the calls to get the latest updates when come back from another screen ¯_(ツ)_/¯. We’ll go through the process of making our app fully reactive. No more refresh.

Needless to advocate much for the benefits clean architecture has to offer. It’s here to stay. It ensures that components with different purposes will sit at their respective places thus enable us to concern about only one thing at a place. It makes sure the more important business logic doesn’t depend on the less important implementation details. The list goes on and on. On the other hand, MVI is just a view layer pattern so it still needs something which is capable of all these cool things. Then comes the clean MVI.

khunzohn

July 09, 2019
Tweet

Other Decks in Programming

Transcript

  1. History Codigo Founded Codigo 2010 2012 Creative Director 2019 Best

    Agency Award Vietnam Dev Team Grow SG Dev Team Myanmar Dev Team 2015 2017 2017 2014 188 Apps
  2. MVP Problems Faced: - Presenter can still be messy -

    Different states in view layer and business logic layer Codigo
  3. MVP Problems Faced: - Presenter can still be messy -

    Different states in view layer and business logic layer - Overlapping states Codigo
  4. What has changed? MVI Ease of Maintenance Clean Style Security

    App Performance Best practices ProGuard Architecture Quicker Bug fix Features CI/ CD Code Quality Encryption Sensitive Data Codigo
  5. Well, what is concern actually? • User interface • Presentation

    • Business Logic • Network Request • Database Codigo
  6. Codigo Clean Architecture Presenter Use Case Output Port Use Case

    Use Use Case Input Port Controller Flow of control <Interface> <Interface>
  7. Codigo Clean Architecture Presenter Use Case Output Port Use Case

    Use Use Case Input Port Flow of control <Interface> <Interface>
  8. fun fetchHotels() { getView().showLoading() fetchHotelsFromServer(object: Callback { override fun onSuccess(hotels:

    List<Hotel>) { getView().stopLoading() getView().renderHotels(hotels) } override fun onError(error: Throwable) { } }) } Codigo
  9. fun fetchHotels() { getView().showLoading() fetchHotelsFromServer(object: Callback { override fun onSuccess(hotels:

    List<Hotel>) { getView().stopLoading() getView().renderHotels(hotels) } override fun onError(error: Throwable) { getView().stopLoading() getView().showError(error) } }) } Codigo
  10. fun fetchHotels() { getView().showLoading() fetchHotelsFromServer(object: Callback { override fun onSuccess(hotels:

    List<Hotel>) { getView().stopLoading() getView().renderHotels(hotels) } override fun onError(error: Throwable) { getView().stopLoading() getView().showError(error) } }) } Codigo
  11. fun fetchHotels() { getView().showLoading() fetchHotelsFromServer(object: Callback { override fun onSuccess(hotels:

    List<Hotel>) { getView().stopLoading() getView().renderHotels(hotels) } override fun onError(error: Throwable) { getView().stopLoading() getView().showError(error) } }) } Codigo
  12. fun fetchHotels() { getView().showLoading() fetchHotelsFromServer(object: Callback { override fun onSuccess(hotels:

    List<Hotel>) { getView().stopLoading() getView().renderHotels(hotels) } override fun onError(error: Throwable) { getView().stopLoading() getView().showError(error) } }) } Codigo
  13. fun fetchHotels() { loading.onNext(true) fetchHotelsFromServer(object: Callback { override fun onSuccess(hotels:

    List<Hotel>) { loading.onNext(false) hotelList.onNext(hotels) } override fun onError(error: Throwable) { } }) } Codigo
  14. fun fetchHotels() { loading.onNext(true) fetchHotelsFromServer(object: Callback { override fun onSuccess(hotels:

    List<Hotel>) { loading.onNext(false) hotelList.onNext(hotels) } override fun onError(error: Throwable) { loading.onNext(false) error.onNext(error) } }) } Codigo
  15. data class HotelViewState( val hotels: List<Hotel> = emptyList(), val loading:

    Boolean = false, val error: Throwable? = null ) Codigo
  16. fun fetchHotels() { getView().render(HotelViewState(loading = true) fetchHotelsFromServer(object: Callback { override

    fun onSuccess(hotels: List<Hotel>) { getView().render(HotelViewState(hotels = hotels) } override fun onError(error: Throwable) { } }) } Codigo
  17. fun fetchHotels() { getView().render(HotelViewState(loading = true) fetchHotelsFromServer(object: Callback { override

    fun onSuccess(hotels: List<Hotel>) { getView().render(HotelViewState(hotels = hotels) } override fun onError(error: Throwable) { getView().render(HotelViewState(error = error) } }) } Codigo
  18. class HotelInteractor constructor ( private val hotelRepo: HotelRepository ) {

    fun fetchHotels() : Observable<HotelViewState> { } } Codigo
  19. class HotelInteractor constructor ( private val hotelRepo: HotelRepository ) {

    fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() } } Codigo
  20. class HotelInteractor constructor ( private val hotelRepo: HotelRepository ) {

    fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() .map { HotelViewState(hotels = it) } } } Codigo
  21. class HotelInteractor constructor ( private val hotelRepo: HotelRepository ) {

    fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() .map { HotelViewState(hotels = it) } .startWith(HotelViewState(loading = true)) } } Codigo
  22. class HotelInteractor constructor ( private val hotelRepo: HotelRepository ) {

    fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() .map { HotelViewState(hotels = it) } .startWith(HotelViewState(loading = true)) .onErrorReturn { HotelViewState(error = it) } } } Codigo
  23. class HotelInteractor constructor ( private val hotelRepo: HotelRepository, private val

    backgroundThread: BackgroundThread ) { fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() .map { HotelViewState(hotels = it) } .startWith(HotelViewState(loading = true)) .onErrorReturn { HotelViewState(error = it) } } } Codigo
  24. class HotelInteractor constructor ( private val hotelRepo: HotelRepository, private val

    backgroundThread: BackgroundThread, private val uiThread: UiThread ) { fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() .map { HotelViewState(hotels = it) } .startWith(HotelViewState(loading = true)) .onErrorReturn { HotelViewState(error = it) } } } Codigo
  25. class HotelInteractor constructor ( private val hotelRepo: HotelRepository, private val

    backgroundThread: BackgroundThread, private val uiThread: UiThread ) { fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() .map { HotelViewState(hotels = it) } .startWith(HotelViewState(loading = true)) .onErrorReturn { HotelViewState(error = it) } .subscribeOn(backgroundThread.getScheduler()) } } Codigo
  26. class HotelInteractor constructor ( private val hotelRepo: HotelRepository, private val

    backgroundThread: BackgroundThread, private val uiThread: UiThread ) { fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() .map { HotelViewState(hotels = it) } .startWith(HotelViewState(loading = true)) .onErrorReturn { HotelViewState(error = it) } .subscribeOn(backgroundThread.getScheduler()) .observeOn(uiThread.getScheduler()) } } Codigo
  27. class HotelInteractor constructor ( private val hotelRepo: HotelRepository, private val

    backgroundThread: BackgroundThread, private val uiThread: UiThread ) { fun fetchHotels() : Observable<HotelViewState> { return hotelRepo.fetchHotels() .map { HotelViewState(hotels = it)} .startWith(HotelViewState(loading = true)) .onErrorReturn { HotelViewState(error = it)} .subscribeOn(backgroundThread.getScheduler()) .observeOn(uiThread.getScheduler()) } } Codigo
  28. class HotelActivity: HomeView { fun fetchHotelsIntent() : Observable<Any> { }

    fun render(viewState: HotelViewState) { } } Codigo
  29. class HotelActivity: HomeView { private val fetchHotelsSubject = PublishSubject.create<Any>() fun

    fetchHotelsIntent() : Observable<Any> { } fun render(viewState: HotelViewState) { } } Codigo
  30. class HotelActivity: HomeView { private val fetchHotelsSubject = PublishSubject.create<Any>() fun

    onResume() { super.onResume() fetchHotelsSubject.onNext(Any()) } fun fetchHotelsIntent() : Observable<Any> { } fun render(viewState: HotelViewState) { } } Codigo
  31. class HotelActivity: HomeView { private val fetchHotelsSubject = PublishSubject.create<Any>() fun

    onResume() { super.onResume() fetchHotelsSubject.onNext(Any()) } fun fetchHotelsIntent() : Observable<Any> { return fetchHotelsSubject } fun render(viewState: HotelViewState) { } } Codigo
  32. class HotelActivity: HomeView { private val fetchHotelsSubject = PublishSubject.create<Any>() fun

    onResume() { super.onResume() fetchHotelsSubject.onNext(Any()) } fun fetchHotelsIntent() : Observable<Any> { return fetchHotelsSubject } fun render(viewState: HotelViewState) { progressBar.visibility = if (viewState.loading) View.VISIBLE else View.GONE } } Codigo
  33. class HotelActivity: HomeView { private val fetchHotelsSubject = PublishSubject.create<Any>() fun

    onResume() { super.onResume() fetchHotelsSubject.onNext(Any()) } fun fetchHotelsIntent() : Observable<Any> { return fetchHotelsSubject } fun render(viewState: HotelViewState) { progressBar.visibility = if (viewState.loading) View.VISIBLE else View.GONE tvError.visibility = if (viewState.error != null) View.VISIBLE else View.GONE tvError.text = viewState.error?.localizedMessage } } Codigo
  34. class HotelActivity: HomeView { private val fetchHotelsSubject = PublishSubject.create<Any>() fun

    onResume() { super.onResume() fetchHotelsSubject.onNext(Any()) } fun fetchHotelsIntent() : Observable<Any> { return fetchHotelsSubject } fun render(viewState: HotelViewState) { progressBar.visibility = if (viewState.loading) View.VISIBLE else View.GONE tvError.visibility = if (viewState.error != null) View.VISIBLE else View.GONE tvError.text = viewState.error?.localizedMessage adapter.setHotelList(viewState.hotels) } } Codigo
  35. class HotelActivity: HomeView { private val fetchHotelsSubject = PublishSubject.create<Any>() fun

    onResume() { super.onResume() fetchHotelsSubject.onNext(Any()) } fun fetchHotelsIntent() : Observable<Any> { return fetchHotelsSubject } fun render(viewState: HotelViewState) { progressBar.visibility = if (viewState.loading) View.VISIBLE else View.GONE tvError.visibility = if (viewState.error != null) View.VISIBLE else View.GONE tvError.text = viewState.error?.localizedMessage adapter.setHotelList(viewState.hotels) } } Codigo
  36. // Internally create PublishSubject that subscribes to intents from the

    view fun intent(binder) : Observable<I> Codigo
  37. // Internally create PublishSubject that subscribes to intents from the

    view fun intent(binder) : Observable<I> // Internally create a BehaviorSubject that subscribes to viewStates emitted by the // business logic fun subscribeToViewState(viewStateObservable, consumer) Codigo
  38. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { } } Codigo
  39. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsIntent = intent { hotelView -> hotelView.fetchHotelsIntent() } } } Codigo
  40. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsIntent: Observable<Any> = intent { hotelView -> hotelView.fetchHotelsIntent() } } } Codigo
  41. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsIntent: Observable<Any> = intent { hotelView -> hotelView.fetchHotelsIntent() } val fetchHotelsState = fetchHotelsIntent .switchMap { interactor.fetchHotels() } } } Codigo
  42. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsIntent: Observable<Any> = intent { hotelView -> hotelView.fetchHotelsIntent() } val fetchHotelsState: Observable<HotelViewState> = fetchHotelsIntent .switchMap { interactor.fetchHotels() } } } Codigo
  43. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsIntent: Observable<Any> = intent { hotelView -> hotelView.fetchHotelsIntent() } val fetchHotelsState: Observable<HotelViewState> = fetchHotelsIntent .switchMap { interactor.fetchHotels() } subscribeViewState() } fun unbindIntents() { } } Codigo
  44. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsIntent: Observable<Any> = intent { hotelView -> hotelView.fetchHotelsIntent() } val fetchHotelsState: Observable<HotelViewState> = fetchHotelsIntent .switchMap { interactor.fetchHotels() } subscribeViewState(fetchHotelsState) } fun unbindIntents() { } } Codigo
  45. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsIntent: Observable<Any> = intent { hotelView -> hotelView.fetchHotelsIntent() } val fetchHotelsState: Observable<HotelViewState> = fetchHotelsIntent .switchMap { interactor.fetchHotels() } subscribeViewState(fetchHotelsState, { hotelView, viewState -> hotelView.render(viewState) }) } fun unbindIntents() { } } Codigo
  46. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsIntent: Observable<Any> = intent { hotelView -> hotelView.fetchHotelsIntent() } val fetchHotelsState: Observable<HotelViewState> = fetchHotelsIntent .switchMap { interactor.fetchHotels() } subscribeViewState(fetchHotelsState, { hotelView, viewState -> hotelView.render(viewState) }) } fun unbindIntents() { } } Codigo
  47. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsState: Observable<HotelViewState> = intent { hotelView -> hotelView.fetchHotelsIntent() } .switchMap { interactor.fetchHotels() } subscribeViewState(fetchHotelsState, { hotelView, viewState -> hotelView.render(viewState) }) } fun unbindIntents() { } } Codigo
  48. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsState = intent { it.fetchHotelsIntent() } .switchMap { interactor.fetchHotels() } subscribeViewState(fetchHotelsState, { hotelView, viewState -> hotelView.render(viewState) }) } fun unbindIntents() { } } Codigo
  49. interface HotelView { fun fetchHotelsIntent() : Observable<Any> fun fetchAirportsIntent() :

    Observable<Any> fun render(viewState: HotelViewState) } Codigo
  50. data class HotelViewState( val hotels: List<Hotel> = emptyList(), val loading:

    Boolean = false, val error: Throwable? = null ) Codigo
  51. data class HotelViewState( val hotels: List<Hotel> = emptyList(), val loading:

    Boolean = false, val error: Throwable? = null, val airports: List<Airport> = emptyList(), val loadingAirport: Boolean = false, val loadAirportError: Throwable? = null ) Codigo
  52. data class HotelViewState( val hotels: List<Hotel> = emptyList(), val loadingHotels:

    Boolean = false, val loadHotelError: Throwable? = null, val airports: List<Airport> = emptyList(), val loadingAirports: Boolean = false, val loadAirportsError: Throwable? = null ) Codigo
  53. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsState = intent { it.fetchHotelsIntent() } .switchMap { interactor.fetchHotels() } val fetchAirportsState = intent { it.fetchAirportsIntent() } .switchMap { interactor.fetchAirports() } subscribeViewState(fetchHotelsState, { hotelView, viewState -> hotelView.render(viewState) }) } } Codigo
  54. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelViewState> {

    ... } fun fetchAirports() : Observable<HotelViewState> { } } Codigo
  55. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelViewState> {

    ... } fun fetchAirports() : Observable<HotelViewState> { return hotelRepo.fetchAirports() .map { HotelViewState(airports = it)} } } Codigo
  56. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelViewState> {

    ... } fun fetchAirports() : Observable<HotelViewState> { return hotelRepo.fetchAirports() .map { HotelViewState(airports = it)} .startWith(HotelViewState(loadingAirport = true)) } } Codigo
  57. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelViewState> {

    ... } fun fetchAirports() : Observable<HotelViewState> { return hotelRepo.fetchAirports() .map { HotelViewState(airports = it)} .startWith(HotelViewState(loadingAirport = true)) .onErrorReturn { HotelViewState(loadAirportError = it)} } } Codigo
  58. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelViewState> {

    ... } fun fetchAirports() : Observable<HotelViewState> { return hotelRepo.fetchAirports() .map { HotelViewState(airports = it)} .startWith(HotelViewState(loadingAirport = true)) .onErrorReturn { HotelViewState(loadAirportError = it)} .subscribeOn(backgroundThread.getScheduler()) .observeOn(uiThread.getScheduler()) } } Codigo
  59. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelViewState> {

    ... } fun fetchAirports() : Observable<HotelViewState> { return hotelRepo.fetchAirports() .map { HotelViewState(airports = it)} .startWith(HotelViewState(loadingAirport = true)) .onErrorReturn { HotelViewState(loadAirportError = it)} .subscribeOn(backgroundThread.getScheduler()) .observeOn(uiThread.getScheduler()) } } Codigo
  60. seal class HotelPartialState { abstract fun reduce(oldState: HotelViewState) : HotelViewState

    object LoadingHotels : HotelPartialState() { override fun reduce(oldState: HotelViewState) : HotelViewState { } } } Codigo
  61. seal class HotelPartialState { abstract fun reduce(oldState: HotelViewState) : HotelViewState

    object LoadingHotels : HotelPartialState() { override fun reduce(oldState: HotelViewState) : HotelViewState { oldState.copy( loadingHotels = true, loadHotelsError = null ) } } } Codigo
  62. seal class HotelPartialState { abstract fun reduce(oldState: HotelViewState) : HotelViewState

    data class HotelsResult( val hotels: List<Hotel> ) : HotelPartialState() { override fun reduce(oldState: HotelViewState) : HotelViewState { } } } Codigo
  63. seal class HotelPartialState { abstract fun reduce(oldState: HotelViewState) : HotelViewState

    data class HotelsResult( val hotels: List<Hotel> ) : HotelPartialState() { override fun reduce(oldState: HotelViewState) : HotelViewState { oldState.copy( hotels = hotels, loadingHotels = false, loadHotelsError = null ) } } } Codigo
  64. seal class HotelPartialState { abstract fun reduce(oldState: HotelViewState) : HotelViewState

    data class AirportsResult( val airports: List<Airport> ) : HotelPartialState() { override fun reduce(oldState: HotelViewState) : HotelViewState { } } } Codigo
  65. seal class HotelPartialState { abstract fun reduce(oldState: HotelViewState) : HotelViewState

    data class AirportsResult( val airports: List<Airport> ) : HotelPartialState() { override fun reduce(oldState: HotelViewState) : HotelViewState { oldState.copy( airports = airports, loadingAirports = false, loadAirportsError = null ) } } } Codigo
  66. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelPartialState> {

    ... } fun fetchAirports() : Observable<HotelPartialState> { ... } } Codigo
  67. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelPartialState> {

    return hotelRepo.fetchHotels() .map { HotelPartialState.HotelsResult(it) } } fun fetchAirports() : Observable<HotelPartialState> { ... } } Codigo
  68. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelPartialState> {

    return hotelRepo.fetchHotels() .map { HotelPartialState.HotelsResult(it) } .startWith(HotelPartialState.LoadingHotels) } fun fetchAirports() : Observable<HotelPartialState> { ... } } Codigo
  69. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelPartialState> {

    return hotelRepo.fetchHotels() .map { HotelPartialState.HotelsResult(it) } .startWith(HotelPartialState.LoadingHotels) .onErrorReturn { HotelPartialState.LoadHotelsError(it) } } fun fetchAirports() : Observable<HotelPartialState> { ... } } Codigo
  70. class HotelInteractor constructor (...) { fun fetchHotels() : Observable<HotelPartialState> {

    return hotelRepo.fetchHotels() .map { HotelPartialState.HotelsResult(it) } .startWith(HotelPartialState.LoadingHotels) .onErrorReturn { HotelPartialState.LoadHotelsError(it) } .subscribeOn(backgroundThread.getScheduler()) .observeOn(uiThread.getScheduler()) } fun fetchAirports() : Observable<HotelPartialState> { ... } } Codigo
  71. class HotelPresenter constructor ( private val interactor: HotelInteractor ) :

    MviBasePresenter<..> { fun bindIntents() { val fetchHotelsState = intent { it.fetchHotelsIntent() } .switchMap { interactor.fetchHotels() } val fetchAirportsState = intent { it.fetchAirportsIntent() } .switchMap { interactor.fetchAirports() } } } Codigo
  72. class HotelPresenter constructor ( ...) : MviBasePresenter<..> { fun bindIntents()

    { val fetchHotelsState = ... val fetchAirportsState = ... val states: Observable<HotelPartialState> = Observable.merge( fetchHotelsState, fetchAirportsState ) } } Codigo
  73. class HotelPresenter constructor ( ...) : MviBasePresenter<..> { fun bindIntents()

    { val fetchHotelsState = ... val fetchAirportsState = ... val states: Observable<HotelPartialState> = Observable.merge( fetchHotelsState, fetchAirportsState ) val reducedStates : Observable<HotelViewState> = states.scan(HotelViewState(), { oldState, partialState -> partialState.reduce(oldState) } } } Codigo
  74. class HotelPresenter constructor ( ...) : MviBasePresenter<..> { fun bindIntents()

    { val fetchHotelsState = ... val fetchAirportsState = ... val states: Observable<HotelPartialState> = Observable.merge( fetchHotelsState, fetchAirportsState ) val reducedStates : Observable<HotelViewState> = states.scan(HotelViewState(), { oldState, partialState -> partialState.reduce(oldState) } subscribeViewState(reducedStates) { hotelView, viewState -> hotelView.render(viewState) } } } Codigo
  75. Codigo • Use DiffUtil to render RecyclerView efficiently. • Use

    Epoxy RecyclerView lib to build complex UI.
  76. Codigo 1. Clean architecture by Uncle Bob 2. Architecting Android

    the clean way (Repository + Articles) by Fernando Cejas 3. MVI articles series by Hannes Dorfmann 4. Mosby library for mvi by Hannes Dorfmann 5. Using DiffUtil in android RecyclerView 6. Epoxy: Airbnb's View Architecture on Android Some useful links