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

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

Benjamin AUGUSTIN

April 07, 2016
Tweet

More Decks by Benjamin AUGUSTIN

Other Decks in Programming

Transcript

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

    reactive Unidirectional Data Flow ?
  2. • Apps get more complex • Predictability • Forces us

    to do things right What’s the point ?
  3. Kotlin Syntax fun aFunction(parameter: String): Int { return parameter.length }

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

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

    fun showLoadingIndicator() fun showLoadingScreen() fun showData() fun showEmptyScreen() fun showErrorIndicator() fun showErrorScreen() }
  6. 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 ?
  7. 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
  8. val retryListener = object : RetryClickedListener { override fun onRetry()

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

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

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

    val error: Throwable? ) enum class Status { LOADING, IDLE, ERROR } Modeling context
  12. 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
  13. private val catsObserver = object : DataObserver<Cats> { override fun

    onNext(p0: Cats) { catsView.display(p0); } } Displaying data is isolated
  14. 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
  15. 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
  16. val retryListener = object : RetryClickedListener { override fun onRetry()

    { catsService.refreshCats() } } Retry is now an Action
  17. 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
  18. 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
  19. class PersistedCatsService(...) : CatsService { private fun getCats(): Observable<Cat>> {

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

    .compose(asEvent<Cats>()) .subscribe { catsSubject.onNext(it) } } } Refresh is now an Action
  21. 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
  22. 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
  23. private val favCatsObserver = object : DataObserver<FavouriteCats> { override fun

    onNext(favouriteCats: FavouriteCats) { catsView.display(favouriteCats) } } Displaying data is isolated
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. • Aim for stateless UI • Model your context •

    Make your domain responsible • UI displays data • UI sends actions In short