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.

05162bc961c3654218bf1839974a4f35?s=128

Benoît Quenaudon

November 06, 2018
Tweet

Transcript

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

  2. Side-Effects Isolation

  3. class ProfileView {a val navigator: Navigator val nameView: TextView val

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() 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
  5. class ProfileView {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() 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
  6. class ProfileView {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() 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
  7. class ProfileView {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() 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
  8. class ProfileView {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() 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
  9. class ProfileView {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() 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
  10. val appService: AppService val nameSubject = BehaviorSubject.create<String>() 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
  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
  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
  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
  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
  15. How about Side Effects?

  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
  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
  18. class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {t }i }t
  19. sealed class Event { data class EditName(val newName: String) :

    Event() }
  20. sealed class Result { data class ProfileResult(val profile: Profile) :

    Result() data class NameValid(val name: String) : Result() object NameInvalid : Result() }
  21. data class ViewModel( val stuff: Any?, val prefixedName: PrefixedName )a

  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" } }
  23. class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

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

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

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() 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
  27. class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() 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
  28. class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() 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
  29. class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() 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
  30. class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() 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
  31. val nameView: TextView val appService: AppService val nameSubject = BehaviorSubject.create<String>()

    val events = PublishSubject.create<Event>() 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
  32. val nameView: TextView val appService: AppService val nameSubject = BehaviorSubject.create<String>()

    val events = PublishSubject.create<Event>() 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
  33. val events = PublishSubject.create<Event>() 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
  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
  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
  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
  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
  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
  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
  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
  41. One Stream Many Options

  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
  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
  44. events // Observable<Event> .subscribe { viewModel -> }o

  45. events // Observable<Event> <!-- prezenter --> .subscribe { viewModel ->

    }o
  46. Passing events to the Presenter

  47. Option A Passing events to the Presenter

  48. events .subscribe { viewModel -> }o class ProfilePresenter : Consumer<Event>

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

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

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

    Consumer<Event> { override fun accept(event: Event) { event.flatMap {} }a }z View Presenter
  52. Observable.just(1, 2, 3) .subscribe { int -> Observable.just(int * 3)

    .subscribe { println(it) } }
  53. Option B Passing events to the Presenter

  54. events. .subscribe { viewModel -> }o class ProfilePresenter(val events:Observable<Event>) {

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

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

    init { events.flatMap { event -> } }a }z View Presenter
  57. Option C Passing events to the Presenter

  58. presenter.somefunction(events) .subscribe { viewModel -> }o class ProfilePresenter { fun

    somefunction(events: Observable<Event>) { events.flatMap { event -> } } } View Presenter
  59. Passing view models to the View

  60. Option A Passing view models to the View

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

    viewmodels(): Observable<ViewModel> { } } View Presenter
  62. Option B Passing view models to the View

  63. presenter .subscribe { viewModel -> }o class ProfilePresenter: Observable<ViewModel>() {

    override fun subscribeActual(observer: Observer<in ViewModel>) { } } View Presenter
  64. Option C Passing view models to the View

  65. presenter .subscribe() class ProfilePresenter: ObservableSource<ViewModel> { override fun subscribe(observer: Observer<in

    ViewModel>) { }a }z View Presenter
  66. presenter .subscribe(object : Observer<ViewModel> { override fun onComplete() {} override

    fun onSubscribe(d: Disposable) {} override fun onNext(t: ViewModel) {} override fun onError(e: Throwable) {} }) class ProfilePresenter: ObservableSource<ViewModel> { override fun subscribe(observer: Observer<in ViewModel>) { }a }z View Presenter
  67. Observable.wrap(presenter) .subscribe { viewModel -> }o class ProfilePresenter: ObservableSource<ViewModel> {

    override fun subscribe(observer: Observer<in ViewModel>) { }a }z View Presenter
  68. Writing is good. Thinking is better.

  69. fun presenter():

  70. fun presenter(events: Observable<Event>):

  71. fun presenter(events: Observable<Event>): Observable<ViewModel>

  72. interface ObservableTransformer<Upstream, Downstream> {a /** * Applies a function to

    the upstream Observable and returns an ObservableSource with * optionally different element type. */ fun apply(upstream: Observable<Upstream>): ObservableSource<Downstream> }b
  73. interface ObservableTransformer<Upstream, Downstream> {a fun apply(upstream: Observable<Upstream>): ObservableSource<Downstream> }b

  74. interface ObservableTransformer<Event, Downstream> {a fun apply(events: Observable<Event>): ObservableSource<Downstream> }b

  75. interface ObservableTransformer<Event, ViewModel> {a fun apply(events: Observable<Event>): ObservableSource<ViewModel> }b

  76. class Presenter: ObservableTransformer<Event, ViewModel> {a fun apply(events: Observable<Event>): ObservableSource<ViewModel> }b

  77. class Presenter: ObservableTransformer<Event, ViewModel> {a fun apply(events: Observable<Event>): ObservableSource<ViewModel> }b

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

    val testObserver = events // PublishSubject<Event> .compose(presenter) .test() // TestObserver<ViewModel> Test Presenter
  79. class Presenter: ObservableTransformer<Event, ViewModel> {a fun apply(events: Observable<Event>): ObservableSource<ViewModel> }b

    val testObserver = events // PublishSubject<Event> .compose(presenter) .test() // TestObserver<ViewModel> events.onNext(EditName("Georges")) testObserver.assertValue(ViewModel()) Test Presenter
  80. Thinking is good. Config Changes is better.

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

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

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

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

    events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>) {k events.subscribe(this.events) }l fun viewModels() {o return events
 .businessLogic()
 .replay(1)
 .autoConnect(0) }i }u View Presenter
  85. presenter.viewModels() .subscribe { viewModel -> }o class ProfilePresenter {j val

    events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>) {k events.subscribe(this.events) }l fun viewModels() {o return events
 .businessLogic()
 .replay(1)
 .autoConnect(0) }i }u View Presenter
  86. presenter.viewModels() .subscribe { viewModel -> }o
 presenter.passEvents(events) class ProfilePresenter {j

    val events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>) {k events.subscribe(this.events) }l fun viewModels() {o return events
 .businessLogic()
 .replay(1)
 .autoConnect(0) }i }u View Presenter
  87. Unidirectional Data Flow

  88. VIEW PRESENTER

  89. VIEW PRESENTER (BUSINESS LOGIC) READING WRITING

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

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

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

  93. New Feature? No Problem.

  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
  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
  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
  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
  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
  99. class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {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
  100. class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {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
  101. class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {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
  102. class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {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
  103. class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {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
  104. override fun apply(events: Observable<Event>): ObservableSource<ViewModel> {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
  105. Architecture forced us to separate concerns No need to touch

    existing code Easy to test Easy to review
  106. Gotta Immutate 'em All! Unidirectional Data Flow Side-Effects Isolation

  107. Benoît Quenaudon @oldergod Fin