Effective Reactive Architecture

Effective Reactive Architecture

Video: https://youtu.be/7fE5MDQ_Sc4

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

In this talk, you'll:
- Explore how side-effect isolation can help avoiding bugs.
- Learn how to model your data and share it as 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 you can easily deal with collateral side effects, such as navigation.

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

October 24, 2019
Tweet

Transcript

  1. Benoît Quenaudon @oldergod Effective Reactive Architecture

  2. None
  3. Side-Effects Isolation

  4. class ProfileView { val navigator: Navigator val nameView: TextView val

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() { nameSubject .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .subscribe(nameView::setText) } }
  6. class ProfileView { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() { nameSubject .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } }
  7. class ProfileView { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() { nameSubject .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } }
  8. class ProfileView { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() { nameSubject .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } }
  9. class ProfileView { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() { nameSubject .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) }
  10. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) } }
  11. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  12. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  13. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  14. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  15. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  16. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  17. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  18. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  19. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  20. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  21. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  22. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  23. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  24. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  25. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  26. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  27. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  28. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  29. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  30. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  31. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  32. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  33. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  34. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  35. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  36. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  37. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  38. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  39. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  40. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  41. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  42. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  43. None
  44. How about Side Effects?

  45. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  46. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  47. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  48. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  49. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  50. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  51. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  52. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  53. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  54. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  55. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  56. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  57. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  58. fun onStart() { nameSubject .switchMap { name -> appService.profile() .map

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } } .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) } fun populateProfile(profile: Profile) { nameSubject.onNext(profile.name) // rendering other stuff } fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) { nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when { response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) } } }
  59. class ProfileView { val navigator: Navigator val nameView: TextView val

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

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

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

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

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

    { data class PrefixedName( val prefix: String, val name: String ) { fun compute(name:String): String = if (name.isEmpty()) "" else "$prefix $name" } }
  65. class ProfileViewRemix { val navigator: Navigator val nameView: TextView val

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> } }
  67. class ProfileViewRemix { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> .subscribe { viewModel -> } } }
  68. class ProfileViewRemix { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } }
  69. class ProfileViewRemix { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap { event -> } ) } .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } }
  70. class ProfileViewRemix { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } } ) } .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } }
  71. class ProfileViewRemix { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } }
  72. class ProfileViewRemix { val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .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 } } }
  73. val nameView: TextView val appService: AppService val nameSubject = BehaviorSubject.create<String>()

    val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } }
  74. val nameView: TextView val appService: AppService val nameSubject = BehaviorSubject.create<String>()

    val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } }
  75. val nameView: TextView val appService: AppService val nameSubject = BehaviorSubject.create<String>()

    val events = PublishSubject.create<Event>() fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } }
  76. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } }
  77. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }
  78. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  79. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  80. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  81. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  82. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  83. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  84. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  85. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  86. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  87. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  88. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  89. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  90. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  91. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  92. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  93. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  94. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  95. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  96. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  97. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  98. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  99. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  100. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  101. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  102. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  103. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  104. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  105. None
  106. One Stream Let's organize it

  107. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  108. fun onStart() { events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap

    { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff } } fun onSetNameDialogResult(newName: String) { events.onNext(EditName(newName)) }
  109. events // Observable<Event> .subscribe { viewModel -> }

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

    }
  111. None
  112. Passing events to the Presenter

  113. Option A Passing events to the Presenter

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

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

    { override fun accept(event: Event) { } } View Presenter
  116. events.subscribe(presenter) presenter.something().subscribe { viewModel -> } class ProfilePresenter : Consumer<Event>

    { override fun accept(event: Event) { } } View Presenter
  117. class ProfilePresenter : Consumer<Event> { override fun accept(event: Event) {

    event.flatMap {} } } View Presenter events.subscribe(presenter) presenter.something().subscribe { viewModel -> }
  118. Option B Passing events to the Presenter

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

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

    init { events.publish { events -> } } } View Presenter
  121. presenter(events) .subscribe { viewModel -> } class ProfilePresenter(val events:Observable<Event>) {

    init { events.publish { events -> } } } View Presenter
  122. Option C Passing events to the Presenter

  123. presenter.viewModels(events) .subscribe { viewModel -> } class ProfilePresenter { fun

    viewModels(events: Observable<Event>) { events.publish { events -> } } } View Presenter
  124. None
  125. Passing view models to the View

  126. Option A Passing view models to the View

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

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

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

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

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

    ViewModel>) { } } View Presenter
  132. 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>) { } } View Presenter
  133. Observable.wrap(presenter) .subscribe { viewModel -> } class ProfilePresenter: ObservableSource<ViewModel> {

    override fun subscribe(observer: Observer<in ViewModel>) { } } View Presenter
  134. None
  135. Writing is good. Thinking is better.

  136. fun presenter()

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

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

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

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

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

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

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

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

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

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

    val testObserver = events // PublishSubject<Event> .compose(presenter) .test() // TestObserver<ViewModel> events.onNext(EditName("Georges")) testObserver.assertValue(ViewModel()) Test Presenter
  147. Presenter • Don't break the stream • Don't start work

    until observed • Doesn't handle disposables* • Has one entry point • Has one exit point
  148. None
  149. Thinking is good. Config Changes is better.

  150. class ProfilePresenter { } View Presenter

  151. class ProfilePresenter { val events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>)

    { events.subscribe(this.events) } } View Presenter
  152. class ProfilePresenter { val events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>)

    { events.subscribe(this.events) } fun viewModels() { return events.businessLogic() } } View Presenter
  153. class ProfilePresenter { val events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>)

    { events.subscribe(this.events) } private val vmObservable : Observable<MatchesViewState> by lazy(NONE) { events.businessLogic() .replay(1) .autoConnect(0) } fun viewModels() = vmObservable } View Presenter
  154. presenter.viewModels() .subscribe { viewModel -> } View Presenter class ProfilePresenter

    { val events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>) { events.subscribe(this.events) } private val vmObservable : Observable<MatchesViewState> by lazy(NONE) { events.businessLogic() .replay(1) .autoConnect(0) } fun viewModels() = vmObservable }
  155. presenter.viewModels() .subscribe { viewModel -> } presenter.passEvents(events) View Presenter class

    ProfilePresenter { val events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>) { events.subscribe(this.events) } private val vmObservable : Observable<MatchesViewState> by lazy(NONE) { events.businessLogic() .replay(1) .autoConnect(0) } fun viewModels() = vmObservable }
  156. None
  157. Unidirectional Data Flow

  158. VIEW PRESENTER

  159. VIEW PRESENTER (BUSINESS LOGIC) READING WRITING

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

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

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

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

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

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

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

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

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

  169. None
  170. New Feature? No Problem.

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

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

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

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

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

    Event() data class ChangeAvatar(val picture: File) : Event() } data class ViewModel( val stuff: Any?, val prefixedName: PrefixedName, val avatarUri: Uri ) class ProfileViewRemix { fun onStart() { events .compose(presenter) .subscribe { viewModel -> // rendering view model Picasso.get().load(viewModel.avatarUri).into(avatarView) } } }
  176. class ProfilePresenter : ObservableTransformer<Event, ViewModel> { override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> { return events.publish { events -> Observable.merge( events.filterIsInstance<EditName>() .switchMap { event -> appService.setName(event.newName).map { response -> when { response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } } } .startWith(NameValid(event.newName)) } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) } }
  177. class ProfilePresenter : ObservableTransformer<Event, ViewModel> { override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> { return events.publish { events -> Observable.merge( events.filterIsInstance<EditName>().logic() ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) } }
  178. class ProfilePresenter : ObservableTransformer<Event, ViewModel> { override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> { return events.publish { events -> Observable.merge( events.filterIsInstance<EditName>().logic() events.filterIsInstance<ChangeAvatar>() .switchMap { event -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) } }
  179. class ProfilePresenter : ObservableTransformer<Event, ViewModel> { override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> { return events.publish { events -> Observable.merge( events.filterIsInstance<EditName>().logic() events.filterIsInstance<ChangeAvatar>() .switchMap { event -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } .onErrorReturn { throwable -> AvatarInvalid(throwable) } } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) } }) } }
  180. class ProfilePresenter : ObservableTransformer<Event, ViewModel> { override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> { return events.publish { events -> Observable.merge( events.filterIsInstance<EditName>().logic() events.filterIsInstance<ChangeAvatar>() .switchMap { event -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } .onErrorReturn { throwable -> AvatarInvalid(throwable) } } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) is AvatarValid -> previous.copy(avatarUri = result.uri) } }) } }
  181. class ProfilePresenter : ObservableTransformer<Event, ViewModel> { override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> { return events.publish { events -> Observable.merge( events.filterIsInstance<EditName>().logic() events.filterIsInstance<ChangeAvatar>() .switchMap { event -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } .onErrorReturn { throwable -> AvatarInvalid(throwable) } } ) } .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) { 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) ) is AvatarValid -> previous.copy(avatarUri = result.uri) is AvatarInvalid -> TODO() } }) } }
  182. None
  183. Architecture forced us to separate concerns

  184. Architecture forced us to separate concerns No need to touch

    existing code
  185. Architecture forced us to separate concerns No need to touch

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

    existing code Easy to test Easy to review
  187. None
  188. Extension Function ?

  189. events.filterIsInstance<ChangeAvatar>() .switchMap { event -> appService.changeAvatar(event.picture) .map { response ->

    AvatarValid(response.uri) } }
  190. events.filterIsInstance<ChangeAvatar>().changeAvatar() private fun Observable<ChangeAvatar>.changeAvatar(): Observable<Result> { return switchMap { event

    -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } } } events.filterIsInstance<ChangeAvatar>() .switchMap { event -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } }
  191. events.filterIsInstance<ChangeAvatar>().changeAvatar() private fun Observable<ChangeAvatar>.changeAvatar(): Observable<Result> { return switchMap { event

    -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } } } events.filterIsInstance<ChangeAvatar>() .switchMap { event -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } }
  192. None
  193. Navigation

  194. interface Navigator { fun goTo(screen: Any) }

  195. interface Navigator { fun goTo(screen: Any) } interface Launcher {

    fun shareText(text: String): Boolean fun launchUrl(url: String): Boolean fun launchApp(packageName: String): Boolean }
  196. interface Navigator { fun goTo(screen: Any) }

  197. interface Navigator { fun goTo(screen: Any) } class ProfilePresenter( private

    val navigator: Navigator ) : ObservableTransformer<Event, ViewModel> { }
  198. interface Navigator { fun goTo(screen: Any) } class ProfilePresenter( private

    val navigator: Navigator ) : ObservableTransformer<Event, ViewModel> { override fun apply(events: Observable<Event>): ObservableSource<ViewModel> { return events.publish { events -> Observable.merge( events.filterIsInstance<GoToEvent>().goTo() ) } } }
  199. interface Navigator { fun goTo(screen: Any) } class ProfilePresenter( private

    val navigator: Navigator ) : ObservableTransformer<Event, ViewModel> { override fun apply(events: Observable<Event>): ObservableSource<ViewModel> { return events.publish { events -> Observable.merge( events.filterIsInstance<GoToEvent>().goTo() ) } } private fun Observable<GoToEvent>.goTo(): Observable<ViewModel> { return doOnNext { navigator.goTo(SomeScreen) } .ignoreElements() .toObservable() } }
  200. inline fun <T : Any, R : Any> Observable<T>.consumeOnNext( crossinline

    sideEffect: (T) -> Unit ): Observable<R> { return doOnNext { sideEffect(it) } .ignoreElements() .toObservable() } private fun Observable<GoToEvent>.goTo(): Observable<ViewModel> { return consumeOnNext { navigator.goTo(SomeScreen) } }
  201. class FakeNavigator : Navigator { private val navigatedScreens = ArrayDeque<Any>()

    override fun goTo(screen: Any) { navigatedScreens.add(screen) } fun takeNext() = navigatedScreens.pop()!! fun isEmpty() = navigatedScreens.isEmpty() } events.accept(Event) assertThat(navigator.takeNextScreen()).isEqualTo(ExpectedScreen)
  202. None
  203. Gotta Immutate 'em All! Unidirectional Data Flow Side-Effects Isolation

  204. Benoît Quenaudon @oldergod Fin