Let it Flow !

Let it Flow !

Reactive Functional Programming becomes more and more recognised as a great way to build reliable and maintainable software.
Functional programming paradigms such as Immutability and avoiding statefull objects are considered good practices and something to strive for.
That’s great! However it is not always straightforward to apply those principles, let alone to create a whole architecture around them.
Refactoring from an existing code base we will see how we can craft a great architecture and simplify our presentation layer by pushing more concerns as part of our domain.

The code presented in this talk is available here: https://github.com/Dorvaryn/unidirectionalDataFlow

6e2280dee0bb8970a3a0257b56a42f42?s=128

Benjamin AUGUSTIN

April 07, 2016
Tweet

Transcript

  1. Let it Flow! Unidirectional data flow architecture on Android

  2. @Dorvaryn +BenjaminAugustin-Dorvaryn Dorvaryn Benjamin Augustin Android Software Craftsman

  3. What is Unidirectional Data Flow ?

  4. • Remove state in UI • Push based • UI

    reactive Unidirectional Data Flow ?
  5. None
  6. • Apps get more complex • Predictability • Forces us

    to do things right What’s the point ?
  7. Let’s build an app

  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. Let’s start with getting data

  17. None
  18. You can use any language

  19. Kotlin Syntax fun aFunction(parameter: String): Int { return parameter.length }

    var thisCanBeNull: Int? = null val lambda = { x: Int, y: Int -> x + y }
  20. None
  21. Example of Views interface CatsView { fun attach(listener: CatsPresenter.CatClickedListener) fun

    display(cats: Cats) fun display(favouriteCats: FavouriteCats) }
  22. Example of Views interface LoadingView { fun attach(retryListener: RetryClickedListener )

    fun showLoadingIndicator() fun showLoadingScreen() fun showData() fun showEmptyScreen() fun showErrorIndicator() fun showErrorScreen() }
  23. Service interface CatService { fun getCat(id: Int): Observable<Cat> }

  24. fun startPresenting() { catsView.attach(catClickedListener) loadingView.attach(retryListener) loadingView.showLoadingScreen() subscriptions = catsService.getCats() .subscribe(catsObserver)

    } What does not belong here ?
  25. fun startPresenting() { catsView.attach(catClickedListener) loadingView.attach(retryListener) loadingView.showLoadingScreen() subscriptions = catsService.getCats() .subscribe(catsObserver)

    } Loading when we request data
  26. private val catsObserver = object : Observer<Cats> { var displayedCats:

    Cats? = null override fun onNext(cats: Cats) { catsView.display(cats); displayedCats = cats loadingView.showLoadingIndicator () } override fun onError(e: Throwable?) { if (displayedCats == null) { loadingView.showErrorScreen () } else { loadingView.showErrorIndicator () } } override fun onCompleted() { if (displayedCats == null) { loadingView.showEmptyScreen () } else { loadingView.showData() } } What is the problem ?
  27. private val catsObserver = object : Observer<Cats> { var displayedCats:

    Cats? = null override fun onNext(cats: Cats) { catsView.display(cats); displayedCats = cats loadingView.showLoadingIndicator () } override fun onError(e: Throwable?) { if (displayedCats == null) { loadingView.showErrorScreen () } else { loadingView.showErrorIndicator () } } override fun onCompleted() { if (displayedCats == null) { loadingView.showEmptyScreen () } else { loadingView.showData() } } Presenter now has state
  28. val retryListener = object : RetryClickedListener { override fun onRetry()

    { stopPresenting() startPresenting() } } What is wrong here ?
  29. val retryListener = object : RetryClickedListener { override fun onRetry()

    { stopPresenting() startPresenting() } } Retry restarting state
  30. class PersistedCatsService(...) : CatsService { override fun getCats() = repository.readCats()

    .flatMap { updateFromRemoteIfOutdated(it) } .switchIfEmpty(fetchRemoteCats()) } Service is request response based
  31. None
  32. data class Event<T> ( val status: Status, val data: T?,

    val error: Throwable? ) enum class Status { LOADING, IDLE, ERROR } Modeling context
  33. abstract class EventObserver<T>: DataObserver<Event<T>> { override fun onNext(p0: Event<T>) {

    when (p0.status) { Status.LOADING -> onLoading(p0) Status.IDLE -> onIdle(p0) Status.ERROR -> onError(p0) } } abstract fun onLoading(event: Event<T>); abstract fun onIdle(event: Event<T>); abstract fun onError(event: Event<T>); Bit of tooling
  34. fun startPresenting() { catsView.attach(catClickedListener) loadingView.attach(retryListener) subscriptions = catsService.getCats() .subscribe(catsObserver) }

    No more state based actions in Presenter
  35. private val catsObserver = object : DataObserver<Cats> { override fun

    onNext(p0: Cats) { catsView.display(p0); } } Displaying data is isolated
  36. private val catsEventsObserver = object : EventObserver <Cats>() { override

    fun onLoading(event: Event<Cats>) { if (event.data != null) { loadingView.showLoadingIndicator () } else { loadingView.showLoadingScreen () } } override fun onIdle(event: Event<Cats>) { if (event.data != null) { loadingView.showData() } else { loadingView.showEmptyScreen () } } override fun onError(event: Event<Cats>) { if (event.data != null) { loadingView.showErrorIndicator () } else { loadingView.showErrorScreen () } } Displaying data is isolated
  37. private val catsEventsObserver = object : EventObserver <Cats>() { override

    fun onLoading(event: Event<Cats>) { if (event.data != null) { loadingView.showLoadingIndicator () } else { loadingView.showLoadingScreen () } } override fun onIdle(event: Event<Cats>) { if (event.data != null) { loadingView.showData() } else { loadingView.showEmptyScreen () } } override fun onError(event: Event<Cats>) { if (event.data != null) { loadingView.showErrorIndicator () } else { loadingView.showErrorScreen () } } No state in Presenter
  38. val retryListener = object : RetryClickedListener { override fun onRetry()

    { catsService.refreshCats() } } Retry is now an Action
  39. class PersistedCatsService(...) : CatsService { val catsSubject = BehaviorSubject.create( Event<Cats>(Status.IDLE,

    null, null) ) override fun getCatsEvents(): Observable<Event<Cats>> { return catsSubject.asObservable() .startWith(initialiseSubject()) .distinctUntilChanged() } } Service with events
  40. class PersistedCatsService(...) : CatsService { private fun initialiseSubject(): Observable<Event<Cats>> {

    if (isInitialised(catsSubject)) { return Observable.empty() } return repository.readCats() .flatMap { updateFromRemoteIfOutdated (it) } .switchIfEmpty(fetchRemoteCats()) .compose(asEvent<Cats>()) .doOnNext { catsSubject.onNext(it) } } } Service with events
  41. class PersistedCatsService(...) : CatsService { private fun getCats(): Observable<Cat>> {

    return getCatsEvents().compose(asData()) } } Data flow comes from events
  42. class PersistedCatsService(...) : CatsService { override fun refreshCats() { fetchRemoteCats()

    .compose(asEvent<Cats>()) .subscribe { catsSubject.onNext(it) } } } Refresh is now an Action
  43. None
  44. • Stateless UI • Domain is empowered • Background push

    compatible What did we gain ?
  45. None
  46. None
  47. None
  48. None
  49. None
  50. None
  51. data class FavouriteCats( val favourites: Map<Cat, FavouriteState> ) { …

    } Modeling context, again
  52. enum class FavouriteState { FAVOURITE, PENDING_FAVOURITE, UN_FAVOURITE, PENDING_UN_FAVOURITE } Modeling

    context, again
  53. fun startPresenting() { catsView.attach(catClickedListener) loadingView.attach(retryListener) subscriptions.add( catsService.getCatsEvents() .subscribe(catsEventsObserver) ) subscriptions.add(

    catsService.getCats() .subscribe(catsObserver) ) subscriptions.add( favouriteCatsService.getFavouriteCats () .subscribe(favouriteCatsObserver) ) } UI just registers itself
  54. fun startPresenting() { catsView.attach(catClickedListener) loadingView.attach(retryListener) subscriptions.add( catsService.getCatsEvents() .subscribe(catsEventsObserver) ) subscriptions.add(

    catsService.getCats() .subscribe(catsObserver) ) subscriptions.add( favouriteCatsService.getFavouriteCats() .subscribe( favouriteCatsObserver) ) } UI just registers itself
  55. private val favCatsObserver = object : DataObserver<FavouriteCats> { override fun

    onNext(favouriteCats: FavouriteCats) { catsView.display(favouriteCats) } } Displaying data is isolated
  56. fun display(cat: Cat, favouriteState : FavouriteState , ...) { ...

    favouriteIndicator.setImageDrawable (favouriteDrawable (favouriteState )) favouriteIndicator.isEnabled = favouriteState == FavouriteState .FAVOURITE || favouriteState == FavouriteState .UN_FAVOURITE ... } private fun favouriteDrawable(favouriteState : FavouriteState ) = when (favouriteState ) { FavouriteState .FAVOURITE -> android.R.drawable.star_big_on FavouriteState .PENDING_FAVOURITE -> android.R.drawable.star_big_on FavouriteState .PENDING_UN_FAVOURITE -> android.R.drawable.star_big_off FavouriteState .UN_FAVOURITE -> android.R.drawable.star_big_off } View Reacts
  57. override fun onFavouriteClicked(cat: Cat, currentState: FavouriteState) { if (currentState ==

    FavouriteState.FAVOURITE) { favouriteCatsService.removeFromFavourite(cat) } else if (currentState == FavouriteState.UN_FAVOURITE) { favouriteCatsService.addToFavourite(cat) } } Modification are actions
  58. class PersistedFavouriteCatsService (...): FavouriteCatsService { val favouriteCatsSubject: = BehaviorSubject .create(

    Event<FavouriteCats>(Status.IDLE, null, null) ) override fun getFavouriteCatsEvents(): Observable<Event<FavouriteCats>> { return favouriteCatsSubject.asObservable() .startWith(initialiseSubject ()) .distinctUntilChanged () } } Similar structure in the Service
  59. override fun addToFavourite(cat: Cat) { api.addToFavourite (cat) .map { Pair(cat,

    FavouriteState .FAVOURITE) } .onErrorReturn { Pair(cat, FavouriteState .UN_FAVOURITE) } .startWith(Pair(cat, FavouriteState .PENDING_FAVOURITE)) .doOnNext { repository.saveCatFavoriteStatus (it) } .subscribe { val value = favouriteCatsSubject.value val favouriteCats = value.data ?: FavouriteCats(mapOf()) favouriteCatsSubject.onNext(Event.of(favouriteCats.put(it))) } } Modification are actions
  60. override fun addToFavourite(cat: Cat) { api.addToFavourite (cat) .map { Pair(cat,

    FavouriteState. FAVOURITE) } .onErrorReturn { Pair(cat, FavouriteState .UN_FAVOURITE) } .startWith(Pair(cat, FavouriteState .PENDING_FAVOURITE)) .doOnNext { repository.saveCatFavoriteStatus (it) } .subscribe { val value = favouriteCatsSubject.value val favouriteCats = value.data ?: FavouriteCats(mapOf()) favouriteCatsSubject.onNext(Event.of(favouriteCats.put(it))) } } Modification are actions
  61. override fun addToFavourite(cat: Cat) { api.addToFavourite (cat) .map { Pair(cat,

    FavouriteState .FAVOURITE) } .onErrorReturn { Pair(cat, FavouriteState. UN_FAVOURITE) } .startWith(Pair(cat, FavouriteState .PENDING_FAVOURITE)) .doOnNext { repository.saveCatFavoriteStatus (it) } .subscribe { val value = favouriteCatsSubject.value val favouriteCats = value.data ?: FavouriteCats(mapOf()) favouriteCatsSubject.onNext(Event.of(favouriteCats.put(it))) } } Modification are actions
  62. override fun addToFavourite(cat: Cat) { api.addToFavourite (cat) .map { Pair(cat,

    FavouriteState .FAVOURITE) } .onErrorReturn { Pair(cat, FavouriteState .UN_FAVOURITE) } .startWith(Pair(cat, FavouriteState. PENDING_FAVOURITE)) .doOnNext { repository.saveCatFavoriteStatus (it) } .subscribe { val value = favouriteCatsSubject.value val favouriteCats = value.data ?: FavouriteCats(mapOf()) favouriteCatsSubject.onNext(Event.of(favouriteCats.put(it))) } } Modification are actions
  63. override fun addToFavourite(cat: Cat) { api.addToFavourite (cat) .map { Pair(cat,

    FavouriteState .FAVOURITE) } .onErrorReturn { Pair(cat, FavouriteState .UN_FAVOURITE) } .startWith(Pair(cat, FavouriteState .PENDING_FAVOURITE)) .doOnNext { repository.saveCatFavoriteStatus( it) } .subscribe { val value = favouriteCatsSubject.value val favouriteCats = value.data ?: FavouriteCats(mapOf()) favouriteCatsSubject.onNext(Event.of(favouriteCats.put(it))) } } Modification are actions
  64. override fun addToFavourite(cat: Cat) { api.addToFavourite (cat) .map { Pair(cat,

    FavouriteState .FAVOURITE) } .onErrorReturn { Pair(cat, FavouriteState .UN_FAVOURITE) } .startWith(Pair(cat, FavouriteState .PENDING_FAVOURITE)) .doOnNext { repository.saveCatFavoriteStatus (it) } .subscribe { val value = favouriteCatsSubject.value val favouriteCats = value.data ?: FavouriteCats(mapOf()) favouriteCatsSubject.onNext(Event.of(favouriteCats.put(it))) } } Modification are actions
  65. • Stateless UI • Domain is empowered • Eventual consistency

    is now easy What did we gain ?
  66. None
  67. Demo Time !

  68. None
  69. • Aim for stateless UI • Model your context •

    Make your domain responsible • UI displays data • UI sends actions In short
  70. None
  71. @Dorvaryn +BenjaminAugustin-Dorvaryn Dorvaryn https://github.com/Dorvaryn/unidirectionalDataFlow/