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. Let it Flow!
    Unidirectional data flow architecture on Android

    View Slide

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

    View Slide

  3. What is Unidirectional
    Data Flow ?

    View Slide

  4. ● Remove state in UI
    ● Push based
    ● UI reactive
    Unidirectional Data Flow ?

    View Slide

  5. View Slide

  6. ● Apps get more complex
    ● Predictability
    ● Forces us to do things right
    What’s the point ?

    View Slide

  7. Let’s build an app

    View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. Let’s start with getting
    data

    View Slide

  17. View Slide

  18. You can use any language

    View Slide

  19. Kotlin Syntax
    fun aFunction(parameter: String): Int {
    return parameter.length
    }
    var thisCanBeNull: Int? = null
    val lambda = { x: Int, y: Int -> x + y }

    View Slide

  20. View Slide

  21. Example of Views
    interface CatsView {
    fun attach(listener: CatsPresenter.CatClickedListener)
    fun display(cats: Cats)
    fun display(favouriteCats: FavouriteCats)
    }

    View Slide

  22. Example of Views
    interface LoadingView {
    fun attach(retryListener: RetryClickedListener )
    fun showLoadingIndicator()
    fun showLoadingScreen()
    fun showData()
    fun showEmptyScreen()
    fun showErrorIndicator()
    fun showErrorScreen()
    }

    View Slide

  23. Service
    interface CatService {
    fun getCat(id: Int): Observable
    }

    View Slide

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

    View Slide

  25. fun startPresenting() {
    catsView.attach(catClickedListener)
    loadingView.attach(retryListener)
    loadingView.showLoadingScreen()
    subscriptions = catsService.getCats()
    .subscribe(catsObserver)
    }
    Loading when we request data

    View Slide

  26. private val catsObserver = object : Observer {
    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 ?

    View Slide

  27. private val catsObserver = object : Observer {
    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

    View Slide

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

    View Slide

  29. val retryListener = object : RetryClickedListener {
    override fun onRetry() {
    stopPresenting()
    startPresenting()
    }
    }
    Retry restarting state

    View Slide

  30. class PersistedCatsService(...) : CatsService {
    override fun getCats() = repository.readCats()
    .flatMap { updateFromRemoteIfOutdated(it) }
    .switchIfEmpty(fetchRemoteCats())
    }
    Service is request response based

    View Slide

  31. View Slide

  32. data class Event (
    val status: Status,
    val data: T?,
    val error: Throwable?
    )
    enum class Status {
    LOADING,
    IDLE,
    ERROR
    }
    Modeling context

    View Slide

  33. abstract class EventObserver: DataObserver> {
    override fun onNext(p0: Event) {
    when (p0.status) {
    Status.LOADING -> onLoading(p0)
    Status.IDLE -> onIdle(p0)
    Status.ERROR -> onError(p0)
    }
    }
    abstract fun onLoading(event: Event);
    abstract fun onIdle(event: Event);
    abstract fun onError(event: Event);
    Bit of tooling

    View Slide

  34. fun startPresenting() {
    catsView.attach(catClickedListener)
    loadingView.attach(retryListener)
    subscriptions = catsService.getCats()
    .subscribe(catsObserver)
    }
    No more state based actions in Presenter

    View Slide

  35. private val catsObserver = object : DataObserver {
    override fun onNext(p0: Cats) {
    catsView.display(p0);
    }
    }
    Displaying data is isolated

    View Slide

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

    View Slide

  37. private val catsEventsObserver = object : EventObserver () {
    override fun onLoading(event: Event) {
    if (event.data != null) {
    loadingView.showLoadingIndicator ()
    } else {
    loadingView.showLoadingScreen ()
    }
    }
    override fun onIdle(event: Event) {
    if (event.data != null) {
    loadingView.showData()
    } else {
    loadingView.showEmptyScreen ()
    }
    }
    override fun onError(event: Event) {
    if (event.data != null) {
    loadingView.showErrorIndicator ()
    } else {
    loadingView.showErrorScreen ()
    }
    }
    No state in Presenter

    View Slide

  38. val retryListener = object : RetryClickedListener {
    override fun onRetry() {
    catsService.refreshCats()
    }
    }
    Retry is now an Action

    View Slide

  39. class PersistedCatsService(...) : CatsService {
    val catsSubject = BehaviorSubject.create(
    Event(Status.IDLE, null, null)
    )
    override fun getCatsEvents(): Observable> {
    return catsSubject.asObservable()
    .startWith(initialiseSubject())
    .distinctUntilChanged()
    }
    }
    Service with events

    View Slide

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

    View Slide

  41. class PersistedCatsService(...) : CatsService {
    private fun getCats(): Observable> {
    return getCatsEvents().compose(asData())
    }
    }
    Data flow comes from events

    View Slide

  42. class PersistedCatsService(...) : CatsService {
    override fun refreshCats() {
    fetchRemoteCats()
    .compose(asEvent())
    .subscribe { catsSubject.onNext(it) }
    }
    }
    Refresh is now an Action

    View Slide

  43. View Slide

  44. ● Stateless UI
    ● Domain is empowered
    ● Background push
    compatible
    What did we gain ?

    View Slide

  45. View Slide

  46. View Slide

  47. View Slide

  48. View Slide

  49. View Slide

  50. View Slide

  51. data class FavouriteCats(
    val favourites: Map
    ) { … }
    Modeling context, again

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  55. private val favCatsObserver = object : DataObserver {
    override fun onNext(favouriteCats: FavouriteCats) {
    catsView.display(favouriteCats)
    }
    }
    Displaying data is isolated

    View Slide

  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

    View Slide

  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

    View Slide

  58. class PersistedFavouriteCatsService (...): FavouriteCatsService {
    val favouriteCatsSubject: = BehaviorSubject .create(
    Event(Status.IDLE, null, null)
    )
    override fun getFavouriteCatsEvents(): Observable> {
    return favouriteCatsSubject.asObservable()
    .startWith(initialiseSubject ())
    .distinctUntilChanged ()
    }
    }
    Similar structure in the Service

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  65. ● Stateless UI
    ● Domain is empowered
    ● Eventual consistency is
    now easy
    What did we gain ?

    View Slide

  66. View Slide

  67. Demo Time !

    View Slide

  68. View Slide

  69. ● Aim for stateless UI
    ● Model your context
    ● Make your domain responsible
    ● UI displays data
    ● UI sends actions
    In short

    View Slide

  70. View Slide

  71. @Dorvaryn
    +BenjaminAugustin-Dorvaryn
    Dorvaryn
    https://github.com/Dorvaryn/unidirectionalDataFlow/

    View Slide