Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k }i }z
  2. 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
  3. 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
  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) // 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
  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) // 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
  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) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g }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 fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) }z
  8. 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
  9. 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
  10. 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
  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. class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {t }i }t
  16. sealed class Result { data class ProfileResult(val profile: Profile) :

    Result() data class NameValid(val name: String) : Result() object NameInvalid : Result() }
  17. 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" } }
  18. class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {t }i }t
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  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 -> 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
  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 -> 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  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 }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y
  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
  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
  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. events .subscribe { viewModel -> }o class ProfilePresenter : Consumer<Event>

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

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

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

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

    viewmodels(): Observable<ViewModel> { } } View Presenter
  43. presenter .subscribe { viewModel -> }o class ProfilePresenter: Observable<ViewModel>() {

    override fun subscribeActual(observer: Observer<in ViewModel>) { } } View Presenter
  44. 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
  45. Observable.wrap(presenter) .subscribe { viewModel -> }o class ProfilePresenter: ObservableSource<ViewModel> {

    override fun subscribe(observer: Observer<in ViewModel>) { }a }z View Presenter
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. Architecture forced us to separate concerns No need to touch

    existing code Easy to test Easy to review