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 full-size slide

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

    View full-size slide

  3. What is Unidirectional
    Data Flow ?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. Let’s build an app

    View full-size slide

  7. Let’s start with getting
    data

    View full-size slide

  8. You can use any language

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

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

    View full-size slide

  27. 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 full-size slide

  28. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

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

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide