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 Slide

  2. Side-Effects Isolation

    View Slide

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

    View Slide

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

    View Slide

  5. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    [email protected]
    .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 Slide

  6. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    [email protected]
    .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 Slide

  7. class ProfileView {a
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    fun onStart() {k
    [email protected]
    .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 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 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 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 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 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 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 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 Slide

  15. How about Side Effects?

    View 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 Slide

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

    View 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Slide

  41. One Stream
    Many Options

    View 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 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 Slide

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

    View Slide

  45. events // Observable

    .subscribe { viewModel ->
    }o

    View Slide

  46. Passing events to the Presenter

    View Slide

  47. Option A
    Passing events to the Presenter

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  53. Option B
    Passing events to the Presenter

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  57. Option C
    Passing events to the Presenter

    View Slide

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

    View Slide

  59. Passing view models
    to the View

    View Slide

  60. Option A
    Passing view models to the View

    View Slide

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

    View Slide

  62. Option B
    Passing view models to the View

    View Slide

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

    View Slide

  64. Option C
    Passing view models to the View

    View Slide

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

    View 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 Slide

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

    View Slide

  68. Writing is good.
    Thinking is better.

    View Slide

  69. fun presenter():

    View Slide

  70. fun presenter(events: Observable):

    View Slide

  71. fun presenter(events: Observable): Observable

    View 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 Slide

  80. Thinking is good.
    Config Changes is better.

    View Slide

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

    View 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 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 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 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 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 Slide

  87. Unidirectional
    Data
    Flow

    View Slide

  88. VIEW
    PRESENTER

    View Slide

  89. VIEW
    PRESENTER (BUSINESS LOGIC)
    READING
    WRITING

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  93. New Feature?
    No Problem.

    View 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 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 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 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 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 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 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

  107. Benoît Quenaudon @oldergod
    Fin

    View Slide