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

Applying Rx Best Practices to Your Architecture

Applying Rx Best Practices to Your Architecture

Video: https://youtu.be/ohBumH5-3Qs

Your relationship with RxJava doesn't have to be complicated. We find many ways to use it wrong; it is also powerful when used properly. In fact, RxJava can guide you in shaping a sound architecture for your app. We only have to follow a few but decisive principles.

In this talk, we'll:

- See at how side-effect isolation can help avoiding bugs.
- Learn how to share one unique stream between your view and your presenter.
- Discover the ways a unidirectional data flow makes adding new functionality easy and safe.
- Look at how data immutability brings safety to data manipulation.

After this talk, you’ll be able to write a robust and reactive architecture for your app, taking full advantage of RxJava.

Benoît Quenaudon

November 06, 2018
Tweet

More Decks by Benoît Quenaudon

Other Decks in Programming

Transcript

  1. Benoît Quenaudon @oldergod
    Applying Rx Best Practices
    to your Architecture

    View full-size slide

  2. Side-Effects Isolation

    View full-size slide

  3. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    }i
    }z

    View full-size slide

  4. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    nameSubject@
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    }i
    }z

    View full-size slide

  5. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    nameSubject@
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    }i
    }z

    View full-size slide

  6. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    nameSubject@
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    }i
    }z

    View full-size slide

  7. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    nameSubject@
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    }z

    View full-size slide

  8. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    }z

    View full-size slide

  9. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    fun onSetNameDialogResult(newName: String) = setName(newName)
    }z

    View full-size slide

  10. val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    fun onSetNameDialogResult(newName: String) = setName(newName)
    fun setName(newName: String) {f
    nameSubject.onNext(newName)
    }b
    }z

    View full-size slide

  11. fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    fun onSetNameDialogResult(newName: String) = setName(newName)
    fun setName(newName: String) {f
    nameSubject.onNext(newName)
    appService.setName(newName)
    .subscribe { response ->
    when {e
    response.isValid -> nameSubject.onNext(response.name)
    response.isInvalid -> navigator.goTo(response.error)
    }d
    }c
    }b

    View full-size slide

  12. fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    fun onSetNameDialogResult(newName: String) = setName(newName)
    fun setName(newName: String) {f
    nameSubject.onNext(newName)
    appService.setName(newName)
    .subscribe { response ->
    when {e
    response.isValid -> nameSubject.onNext(response.name)
    response.isInvalid -> navigator.goTo(response.error)
    }d
    }c
    }b

    View full-size slide

  13. fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    fun onSetNameDialogResult(newName: String) = setName(newName)
    fun setName(newName: String) {f
    nameSubject.onNext(newName)
    appService.setName(newName)
    .subscribe { response ->
    when {e
    response.isValid -> nameSubject.onNext(response.name)
    response.isInvalid -> navigator.goTo(response.error)
    }d
    }c
    }b

    View full-size slide

  14. fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    fun onSetNameDialogResult(newName: String) = setName(newName)
    fun setName(newName: String) {f
    nameSubject.onNext(newName)
    appService.setName(newName)
    .subscribe { response ->
    when {e
    response.isValid -> nameSubject.onNext(response.name)
    response.isInvalid -> navigator.goTo(response.error)
    }d
    }c
    }b

    View full-size slide

  15. How about Side Effects?

    View full-size slide

  16. fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    fun onSetNameDialogResult(newName: String) = setName(newName)
    fun setName(newName: String) {f
    nameSubject.onNext(newName)
    appService.setName(newName)
    .subscribe { response ->
    when {e
    response.isValid -> nameSubject.onNext(response.name)
    response.isInvalid -> navigator.goTo(response.error)
    }d
    }c
    }b

    View full-size slide

  17. fun onStart() {k
    nameSubject
    .switchMap { name ->
    appService.profile()
    .map { profile -> profile.prefix }
    .map { profile -> if (name.isEmpty()) "" else "$prefix $name" }
    }j
    .subscribe(nameView::setText)
    // hint grows the view in width so hide it if unnecessary
    nameSubject
    .map { name -> if (name.isEmpty()) "Set your name" else "" }
    .subscribe(nameView::setHint)
    appService.profile().subscribe(this::populateProfile)
    }i
    fun populateProfile(profile: Profile) {h
    nameSubject.onNext(profile.name)
    // rendering other stuff
    }g
    fun onSetNameDialogResult(newName: String) = setName(newName)
    fun setName(newName: String) {f
    nameSubject.onNext(newName)
    appService.setName(newName)
    .subscribe { response ->
    when {e
    response.isValid -> nameSubject.onNext(response.name)
    response.isInvalid -> navigator.goTo(response.error)
    }d
    }c
    }b

    View full-size slide

  18. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {t
    }i
    }t

    View full-size slide

  19. sealed class Event {
    data class EditName(val newName: String) : Event()
    }

    View full-size slide

  20. sealed class Result {
    data class ProfileResult(val profile: Profile) : Result()
    data class NameValid(val name: String) : Result()
    object NameInvalid : Result()
    }

    View full-size slide

  21. data class ViewModel(
    val stuff: Any?,
    val prefixedName: PrefixedName
    )a

    View full-size slide

  22. data class ViewModel(
    val stuff: Any?,
    val prefixedName: PrefixedName
    )a{
    data class PrefixedName(
    val prefix: String,
    val name: String
    ) {
    fun compute(name:String): String = if (name.isEmpty()) "" else "$prefix $name"
    }
    }

    View full-size slide

  23. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {t
    }i
    }t

    View full-size slide

  24. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    }i
    }t

    View full-size slide

  25. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    .subscribe { viewModel ->
    }o
    }i
    }t

    View full-size slide

  26. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    }t

    View full-size slide

  27. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName ->
    }g
    }h
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    }t

    View full-size slide

  28. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    }g
    }h
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    }t

    View full-size slide

  29. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    }t

    View full-size slide

  30. class ProfileViewRemix {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    }t

    View full-size slide

  31. val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    }t

    View full-size slide

  32. val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    }t

    View full-size slide

  33. val events = PublishSubject.create()
    fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    }t

    View full-size slide

  34. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o

    View full-size slide

  35. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    fun onSetNameDialogResult(newName: String) {u
    events.onNext(EditName(newName))
    }y
    }t

    View full-size slide

  36. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    fun onSetNameDialogResult(newName: String) {u
    events.onNext(EditName(newName))
    }y
    }t

    View full-size slide

  37. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    fun onSetNameDialogResult(newName: String) {u
    events.onNext(EditName(newName))
    }y

    View full-size slide

  38. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    fun onSetNameDialogResult(newName: String) {u
    events.onNext(EditName(newName))
    }y

    View full-size slide

  39. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    fun onSetNameDialogResult(newName: String) {u
    events.onNext(EditName(newName))
    }y

    View full-size slide

  40. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    fun onSetNameDialogResult(newName: String) {u
    events.onNext(EditName(newName))
    }y

    View full-size slide

  41. One Stream
    Many Options

    View full-size slide

  42. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    fun onSetNameDialogResult(newName: String) {u
    events.onNext(EditName(newName))
    }y

    View full-size slide

  43. fun onStart() {t
    events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )k
    }l
    })p
    .subscribe { viewModel ->
    nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else ""
    nameView.text = viewModel.prefixedName.compute()
    // rendering other stuff
    }o
    }i
    fun onSetNameDialogResult(newName: String) {u
    events.onNext(EditName(newName))
    }y

    View full-size slide

  44. events // Observable
    .subscribe { viewModel ->
    }o

    View full-size slide

  45. events // Observable

    .subscribe { viewModel ->
    }o

    View full-size slide

  46. Passing events to the Presenter

    View full-size slide

  47. Option A
    Passing events to the Presenter

    View full-size slide

  48. events
    .subscribe { viewModel ->
    }o
    class ProfilePresenter : Consumer {
    }z
    View
    Presenter

    View full-size slide

  49. events
    .subscribe { viewModel ->
    }o
    class ProfilePresenter : Consumer {
    override fun accept(event: Event) {
    }a
    }z
    View
    Presenter

    View full-size slide

  50. events
    .subscribe(presenter)
    .subscribe { viewModel ->
    }o
    class ProfilePresenter : Consumer {
    override fun accept(event: Event) {
    }a
    }z
    View
    Presenter

    View full-size slide

  51. events
    .subscribe(presenter)
    .subscribe { viewModel ->
    }o
    class ProfilePresenter : Consumer {
    override fun accept(event: Event) {
    event.flatMap {}
    }a
    }z
    View
    Presenter

    View full-size slide

  52. Observable.just(1, 2, 3)
    .subscribe { int ->
    Observable.just(int * 3)
    .subscribe { println(it) }
    }

    View full-size slide

  53. Option B
    Passing events to the Presenter

    View full-size slide

  54. events.
    .subscribe { viewModel ->
    }o
    class ProfilePresenter(val events:Observable) {
    }z
    View
    Presenter

    View full-size slide

  55. events.
    .subscribe { viewModel ->
    }o
    class ProfilePresenter(val events:Observable) {
    init {
    events.flatMap { event -> }
    }a
    }z
    View
    Presenter

    View full-size slide

  56. presenter(events)
    .subscribe { viewModel ->
    }o
    class ProfilePresenter(val events:Observable) {
    init {
    events.flatMap { event -> }
    }a
    }z
    View
    Presenter

    View full-size slide

  57. Option C
    Passing events to the Presenter

    View full-size slide

  58. presenter.somefunction(events)
    .subscribe { viewModel ->
    }o
    class ProfilePresenter {
    fun somefunction(events: Observable) {
    events.flatMap { event -> }
    }
    }
    View
    Presenter

    View full-size slide

  59. Passing view models
    to the View

    View full-size slide

  60. Option A
    Passing view models to the View

    View full-size slide

  61. presenter.viewmodels()
    .subscribe { viewModel ->
    }o
    class ProfilePresenter {
    fun viewmodels(): Observable {
    }
    }
    View
    Presenter

    View full-size slide

  62. Option B
    Passing view models to the View

    View full-size slide

  63. presenter
    .subscribe { viewModel ->
    }o
    class ProfilePresenter: Observable() {
    override fun subscribeActual(observer: Observer) {
    }
    }
    View
    Presenter

    View full-size slide

  64. Option C
    Passing view models to the View

    View full-size slide

  65. presenter
    .subscribe()
    class ProfilePresenter: ObservableSource {
    override fun subscribe(observer: Observer) {
    }a
    }z
    View
    Presenter

    View full-size slide

  66. presenter
    .subscribe(object : Observer {
    override fun onComplete() {}
    override fun onSubscribe(d: Disposable) {}
    override fun onNext(t: ViewModel) {}
    override fun onError(e: Throwable) {}
    })
    class ProfilePresenter: ObservableSource {
    override fun subscribe(observer: Observer) {
    }a
    }z
    View
    Presenter

    View full-size slide

  67. Observable.wrap(presenter)
    .subscribe { viewModel ->
    }o
    class ProfilePresenter: ObservableSource {
    override fun subscribe(observer: Observer) {
    }a
    }z
    View
    Presenter

    View full-size slide

  68. Writing is good.
    Thinking is better.

    View full-size slide

  69. fun presenter():

    View full-size slide

  70. fun presenter(events: Observable):

    View full-size slide

  71. fun presenter(events: Observable): Observable

    View full-size slide

  72. interface ObservableTransformer {a
    /**
    * Applies a function to the upstream Observable and returns an ObservableSource with
    * optionally different element type.
    */
    fun apply(upstream: Observable): ObservableSource
    }b

    View full-size slide

  73. interface ObservableTransformer {a
    fun apply(upstream: Observable): ObservableSource
    }b

    View full-size slide

  74. interface ObservableTransformer {a
    fun apply(events: Observable): ObservableSource
    }b

    View full-size slide

  75. interface ObservableTransformer {a
    fun apply(events: Observable): ObservableSource
    }b

    View full-size slide

  76. class Presenter: ObservableTransformer {a
    fun apply(events: Observable): ObservableSource
    }b

    View full-size slide

  77. class Presenter: ObservableTransformer {a
    fun apply(events: Observable): ObservableSource
    }b
    events
    .compose(presenter)
    .subscribe { viewModel ->
    }o
    View
    Presenter

    View full-size slide

  78. class Presenter: ObservableTransformer {a
    fun apply(events: Observable): ObservableSource
    }b
    val testObserver = events // PublishSubject
    .compose(presenter)
    .test() // TestObserver
    Test
    Presenter

    View full-size slide

  79. class Presenter: ObservableTransformer {a
    fun apply(events: Observable): ObservableSource
    }b
    val testObserver = events // PublishSubject
    .compose(presenter)
    .test() // TestObserver
    events.onNext(EditName("Georges"))
    testObserver.assertValue(ViewModel())
    Test
    Presenter

    View full-size slide

  80. Thinking is good.
    Config Changes is better.

    View full-size slide

  81. presenter.viewModels()
    .subscribe { viewModel ->
    }o
    class ProfilePresenter {j
    }u
    View
    Presenter

    View full-size slide

  82. presenter.viewModels()
    .subscribe { viewModel ->
    }o
    class ProfilePresenter {j
    val events = PublishSubject.create()
    fun passEvents(events: Observable) {k
    events.subscribe(this.events)
    }l
    }u
    View
    Presenter

    View full-size slide

  83. presenter.viewModels()
    .subscribe { viewModel ->
    }o
    class ProfilePresenter {j
    val events = PublishSubject.create()
    fun passEvents(events: Observable) {k
    events.subscribe(this.events)
    }l
    fun viewModels() {o
    return events

    .businessLogic()
    }i
    }u
    View
    Presenter

    View full-size slide

  84. presenter.viewModels()
    .subscribe { viewModel ->
    }o
    class ProfilePresenter {j
    val events = PublishSubject.create()
    fun passEvents(events: Observable) {k
    events.subscribe(this.events)
    }l
    fun viewModels() {o
    return events

    .businessLogic()

    .replay(1)

    .autoConnect(0)
    }i
    }u
    View
    Presenter

    View full-size slide

  85. presenter.viewModels()
    .subscribe { viewModel ->
    }o
    class ProfilePresenter {j
    val events = PublishSubject.create()
    fun passEvents(events: Observable) {k
    events.subscribe(this.events)
    }l
    fun viewModels() {o
    return events

    .businessLogic()

    .replay(1)

    .autoConnect(0)
    }i
    }u
    View
    Presenter

    View full-size slide

  86. presenter.viewModels()
    .subscribe { viewModel ->
    }o

    presenter.passEvents(events)
    class ProfilePresenter {j
    val events = PublishSubject.create()
    fun passEvents(events: Observable) {k
    events.subscribe(this.events)
    }l
    fun viewModels() {o
    return events

    .businessLogic()

    .replay(1)

    .autoConnect(0)
    }i
    }u
    View
    Presenter

    View full-size slide

  87. Unidirectional
    Data
    Flow

    View full-size slide

  88. VIEW
    PRESENTER

    View full-size slide

  89. VIEW
    PRESENTER (BUSINESS LOGIC)
    READING
    WRITING

    View full-size slide

  90. VIEW
    PRESENTER (BUSINESS LOGIC)
    VIEW (READING) VIEW (WRITING)

    View full-size slide

  91. VIEW (READING)
    VIEW (WRITING)
    PRESENTER (BUSINESS LOGIC)

    View full-size slide

  92. VIEW (READING)
    VIEW (WRITING)
    PRESENTER (BUSINESS LOGIC)

    View full-size slide

  93. New Feature?
    No Problem.

    View full-size slide

  94. sealed class Event {
    data class EditName(val newName: String) : Event()
    }a
    data class ViewModel(
    val stuff: Any?,
    val prefixedName: PrefixedName
    )b
    class ProfileViewRemix {
    fun onStart() {t
    events
    .compose(presenter)
    .subscribe { viewModel ->
    // rendering view model
    }o
    }i
    }t

    View full-size slide

  95. sealed class Event {
    data class EditName(val newName: String) : Event()
    }a
    data class ViewModel(
    val stuff: Any?,
    val prefixedName: PrefixedName
    )b
    class ProfileViewRemix {
    fun onStart() {t
    events
    .compose(presenter)
    .subscribe { viewModel ->
    // rendering view model
    }o
    }i
    }t

    View full-size slide

  96. sealed class Event {
    data class EditName(val newName: String) : Event()
    data class ChangeAvatar(val picture: File) : Event()
    }a
    data class ViewModel(
    val stuff: Any?,
    val prefixedName: PrefixedName
    )b
    class ProfileViewRemix {
    fun onStart() {t
    events
    .compose(presenter)
    .subscribe { viewModel ->
    // rendering view model
    }o
    }i
    }t

    View full-size slide

  97. sealed class Event {
    data class EditName(val newName: String) : Event()
    data class ChangeAvatar(val picture: File) : Event()
    }a
    data class ViewModel(
    val stuff: Any?,
    val prefixedName: PrefixedName,
    val avatarUri: Uri
    )b
    class ProfileViewRemix {
    fun onStart() {t
    events
    .compose(presenter)
    .subscribe { viewModel ->
    // rendering view model
    }o
    }i
    }t

    View full-size slide

  98. sealed class Event {
    data class EditName(val newName: String) : Event()
    data class ChangeAvatar(val picture: File) : Event()
    }a
    data class ViewModel(
    val stuff: Any?,
    val prefixedName: PrefixedName,
    val avatarUri: Uri
    )b
    class ProfileViewRemix {
    fun onStart() {t
    events
    .compose(presenter)
    .subscribe { viewModel ->
    // rendering view model
    Picasso.get().load(viewModel.avatarUri).into(avatarView)
    }o
    }i
    }t

    View full-size slide

  99. class ProfilePresenter : ObservableTransformer {a
    override fun apply(events: Observable): ObservableSource {d
    return events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )f
    }g
    })h
    }j
    }k

    View full-size slide

  100. class ProfilePresenter : ObservableTransformer {a
    override fun apply(events: Observable): ObservableSource {d
    return events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s
    }d
    }f
    .startWith(NameValid(event.newName))
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )f
    }g
    })h
    }j
    }k

    View full-size slide

  101. class ProfilePresenter : ObservableTransformer {a
    override fun apply(events: Observable): ObservableSource {d
    return events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s
    }d
    }f
    .startWith(NameValid(event.newName))
    is ChangeAvatar -> appService.changeAvatar(event.picture)
    .map { response -> AvatarValid(response.uri) }
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )f
    }g
    })h
    }j
    }k

    View full-size slide

  102. class ProfilePresenter : ObservableTransformer {a
    override fun apply(events: Observable): ObservableSource {d
    return events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s
    }d
    }f
    .startWith(NameValid(event.newName))
    is ChangeAvatar -> appService.changeAvatar(event.picture)
    .map { response -> AvatarValid(response.uri) }
    .onErrorReturn { throwable -> AvatarInvalid(throwable) }
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )f
    }g
    })h
    }j
    }k

    View full-size slide

  103. class ProfilePresenter : ObservableTransformer {a
    override fun apply(events: Observable): ObservableSource {d
    return events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s
    }d
    }f
    .startWith(NameValid(event.newName))
    is ChangeAvatar -> appService.changeAvatar(event.picture)
    .map { response -> AvatarValid(response.uri) }
    .onErrorReturn { throwable -> AvatarInvalid(throwable) }
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )f
    is AvatarValid -> previous.copy(avatarUri = result.uri)
    }g
    })h
    }j

    View full-size slide

  104. override fun apply(events: Observable): ObservableSource {d
    return events.flatMap { event ->
    when (event) {y
    is EditName -> appService.setName(event.newName)
    .map { response ->
    when {u
    response.isValid -> NameValid(response.name)
    response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s
    }d
    }f
    .startWith(NameValid(event.newName))
    is ChangeAvatar -> appService.changeAvatar(event.picture)
    .map { response -> AvatarValid(response.uri) }
    .onErrorReturn { throwable -> AvatarInvalid(throwable) }
    }g
    }h
    .mergeWith(appService.profile().map(::ProfileResult))
    .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")),
    BiFunction { previous: ViewModel, result: Result ->
    when (result) {j
    is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name))
    is NameInvalid -> previous
    is ProfileResult -> previous.copy(
    stuff = result.profile,
    prefixedName = PrefixedName(result.profile.prefix, result.profile.name)
    )f
    is AvatarValid -> previous.copy(avatarUri = result.uri)
    is AvatarInvalid -> TODO()
    }g
    })h
    }j

    View full-size slide

  105. Architecture forced us to separate concerns
    No need to touch existing code
    Easy to test
    Easy to review

    View full-size slide

  106. Gotta Immutate 'em All!
    Unidirectional Data Flow
    Side-Effects Isolation

    View full-size slide

  107. Benoît Quenaudon @oldergod
    Fin

    View full-size slide