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

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.

Benoît Quenaudon

October 24, 2019
Tweet

More Decks by Benoît Quenaudon

Other Decks in Programming

Transcript

  1. Benoît Quenaudon @oldergod
    Effective Reactive Architecture

    View Slide

  2. View Slide

  3. Side-Effects Isolation

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  43. View Slide

  44. How about Side Effects?

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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"
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  70. class ProfileViewRemix {
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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
    }
    }
    }

    View Slide

  71. class ProfileViewRemix {
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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
    }
    }

    View Slide

  72. class ProfileViewRemix {
    val navigator: Navigator
    val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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
    }
    }
    }

    View Slide

  73. val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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
    }
    }
    }

    View Slide

  74. val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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
    }
    }
    }

    View Slide

  75. val nameView: TextView
    val appService: AppService
    val nameSubject = BehaviorSubject.create()
    val events = PublishSubject.create()
    fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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
    }
    }
    }

    View Slide

  76. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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
    }
    }
    }

    View Slide

  77. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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
    }

    View Slide

  78. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  79. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  80. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  81. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  82. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  83. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  84. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  85. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  86. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  87. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  88. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  89. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  90. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  91. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  92. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  93. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  94. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  95. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  96. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  97. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  98. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  99. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  100. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  101. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  102. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  103. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  104. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  105. View Slide

  106. One Stream
    Let's organize it

    View Slide

  107. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  108. fun onStart() {
    events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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))
    }

    View Slide

  109. events // Observable
    .subscribe { viewModel ->
    }

    View Slide

  110. events // Observable

    .subscribe { viewModel ->
    }

    View Slide

  111. View Slide

  112. Passing events to the Presenter

    View Slide

  113. Option A
    Passing events to the Presenter

    View Slide

  114. events
    .subscribe { viewModel ->
    }
    class ProfilePresenter : Consumer {
    }
    View
    Presenter

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  118. Option B
    Passing events to the Presenter

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  122. Option C
    Passing events to the Presenter

    View Slide

  123. presenter.viewModels(events)
    .subscribe { viewModel ->
    }
    class ProfilePresenter {
    fun viewModels(events: Observable) {
    events.publish { events -> }
    }
    }
    View
    Presenter

    View Slide

  124. View Slide

  125. Passing view models
    to the View

    View Slide

  126. Option A
    Passing view models to the View

    View Slide

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

    View Slide

  128. Option B
    Passing view models to the View

    View Slide

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

    View Slide

  130. Option C
    Passing view models to the View

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  134. View Slide

  135. Writing is good.
    Thinking is better.

    View Slide

  136. fun presenter()

    View Slide

  137. fun presenter(events: Observable)

    View Slide

  138. fun presenter(events: Observable): Observable

    View Slide

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

    View Slide

  140. interface ObservableTransformer {
    fun apply(upstream: Observable): ObservableSource
    }

    View Slide

  141. interface ObservableTransformer {
    fun apply(events: Observable): ObservableSource
    }

    View Slide

  142. interface ObservableTransformer {
    fun apply(events: Observable): ObservableSource
    }

    View Slide

  143. class Presenter: ObservableTransformer {
    fun apply(events: Observable): ObservableSource
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  148. View Slide

  149. Thinking is good.
    Config Changes is better.

    View Slide

  150. class ProfilePresenter {
    }
    View
    Presenter

    View Slide

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

    View Slide

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

    View Slide

  153. class ProfilePresenter {
    val events = PublishSubject.create()
    fun passEvents(events: Observable) {
    events.subscribe(this.events)
    }
    private val vmObservable : Observable by lazy(NONE) {
    events.businessLogic()
    .replay(1)
    .autoConnect(0)
    }
    fun viewModels() = vmObservable
    }
    View
    Presenter

    View Slide

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

    View Slide

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

    View Slide

  156. View Slide

  157. Unidirectional
    Data
    Flow

    View Slide

  158. VIEW
    PRESENTER

    View Slide

  159. VIEW
    PRESENTER (BUSINESS LOGIC)
    READING
    WRITING

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  169. View Slide

  170. New Feature?
    No Problem.

    View Slide

  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
    }
    }
    }

    View Slide

  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
    }
    }
    }

    View Slide

  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
    }
    }
    }

    View Slide

  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
    }
    }
    }

    View Slide

  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)
    }
    }
    }

    View Slide

  176. class ProfilePresenter : ObservableTransformer {
    override fun apply(events: Observable): ObservableSource {
    return events.publish { events ->
    Observable.merge(
    events.filterIsInstance()
    .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)
    )
    }
    })
    }
    }

    View Slide

  177. class ProfilePresenter : ObservableTransformer {
    override fun apply(events: Observable): ObservableSource {
    return events.publish { events ->
    Observable.merge(
    events.filterIsInstance().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)
    )
    }
    })
    }
    }

    View Slide

  178. class ProfilePresenter : ObservableTransformer {
    override fun apply(events: Observable): ObservableSource {
    return events.publish { events ->
    Observable.merge(
    events.filterIsInstance().logic()
    events.filterIsInstance()
    .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)
    )
    }
    })
    }
    }

    View Slide

  179. class ProfilePresenter : ObservableTransformer {
    override fun apply(events: Observable): ObservableSource {
    return events.publish { events ->
    Observable.merge(
    events.filterIsInstance().logic()
    events.filterIsInstance()
    .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)
    )
    }
    })
    }
    }

    View Slide

  180. class ProfilePresenter : ObservableTransformer {
    override fun apply(events: Observable): ObservableSource {
    return events.publish { events ->
    Observable.merge(
    events.filterIsInstance().logic()
    events.filterIsInstance()
    .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)
    }
    })
    }
    }

    View Slide

  181. class ProfilePresenter : ObservableTransformer {
    override fun apply(events: Observable): ObservableSource {
    return events.publish { events ->
    Observable.merge(
    events.filterIsInstance().logic()
    events.filterIsInstance()
    .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()
    }
    })
    }
    }

    View Slide

  182. View Slide

  183. Architecture forced us to separate concerns

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  187. View Slide

  188. Extension Function ?

    View Slide

  189. events.filterIsInstance()
    .switchMap { event ->
    appService.changeAvatar(event.picture)
    .map { response -> AvatarValid(response.uri) }
    }

    View Slide

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

    View Slide

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

    View Slide

  192. View Slide

  193. Navigation

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  199. interface Navigator {
    fun goTo(screen: Any)
    }
    class ProfilePresenter(
    private val navigator: Navigator
    ) : ObservableTransformer {
    override fun apply(events: Observable): ObservableSource {
    return events.publish { events ->
    Observable.merge(
    events.filterIsInstance().goTo()
    )
    }
    }
    private fun Observable.goTo(): Observable {
    return doOnNext {
    navigator.goTo(SomeScreen)
    }
    .ignoreElements()
    .toObservable()
    }
    }

    View Slide

  200. inline fun Observable.consumeOnNext(
    crossinline sideEffect: (T) -> Unit
    ): Observable {
    return doOnNext { sideEffect(it) }
    .ignoreElements()
    .toObservable()
    }
    private fun Observable.goTo(): Observable {
    return consumeOnNext {
    navigator.goTo(SomeScreen)
    }
    }

    View Slide

  201. class FakeNavigator : Navigator {
    private val navigatedScreens = ArrayDeque()
    override fun goTo(screen: Any) {
    navigatedScreens.add(screen)
    }
    fun takeNext() = navigatedScreens.pop()!!
    fun isEmpty() = navigatedScreens.isEmpty()
    }
    events.accept(Event)
    assertThat(navigator.takeNextScreen()).isEqualTo(ExpectedScreen)

    View Slide

  202. View Slide

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

    View Slide

  204. Benoît Quenaudon @oldergod
    Fin

    View Slide