Applying Rx Best Practices to Your Architecture

Applying Rx Best Practices to Your Architecture

Video: https://youtu.be/ohBumH5-3Qs

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

In this talk, we'll:

- See at how side-effect isolation can help avoiding bugs.
- Learn how to share one unique stream between your view and your presenter.
- Discover the ways a unidirectional data flow makes adding new functionality easy and safe.
- Look at how data immutability brings safety to data manipulation.

After this talk, you’ll be able to write a robust and reactive architecture for your app, taking full advantage of RxJava.

05162bc961c3654218bf1839974a4f35?s=128

Benoît Quenaudon

November 06, 2018
Tweet

Transcript

  1. 3.

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k }i }z
  2. 4.

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k nameSubject@ .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) }i }z
  3. 5.

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k nameSubject@ .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) }i }z
  4. 6.

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k nameSubject@ .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) }i }z
  5. 7.

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k nameSubject@ .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i }z
  6. 8.

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k nameSubject .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g }z
  7. 9.

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

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k nameSubject .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) }z
  8. 10.

    val appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {k

    nameSubject .switchMap { name -> appService.profile() .map { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) {f nameSubject.onNext(newName) }b }z
  9. 11.

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

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) {f nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when {e response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) }d }c }b
  10. 12.

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

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) {f nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when {e response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) }d }c }b
  11. 13.

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

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) {f nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when {e response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) }d }c }b
  12. 14.

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

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) {f nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when {e response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) }d }c }b
  13. 16.

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

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) {f nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when {e response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) }d }c }b
  14. 17.

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

    { profile -> profile.prefix } .map { profile -> if (name.isEmpty()) "" else "$prefix $name" } }j .subscribe(nameView::setText) // hint grows the view in width so hide it if unnecessary nameSubject .map { name -> if (name.isEmpty()) "Set your name" else "" } .subscribe(nameView::setHint) appService.profile().subscribe(this::populateProfile) }i fun populateProfile(profile: Profile) {h nameSubject.onNext(profile.name) // rendering other stuff }g fun onSetNameDialogResult(newName: String) = setName(newName) fun setName(newName: String) {f nameSubject.onNext(newName) appService.setName(newName) .subscribe { response -> when {e response.isValid -> nameSubject.onNext(response.name) response.isInvalid -> navigator.goTo(response.error) }d }c }b
  15. 18.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {t }i }t
  16. 20.

    sealed class Result { data class ProfileResult(val profile: Profile) :

    Result() data class NameValid(val name: String) : Result() object NameInvalid : Result() }
  17. 22.

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

    data class PrefixedName( val prefix: String, val name: String ) { fun compute(name:String): String = if (name.isEmpty()) "" else "$prefix $name" } }
  18. 23.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() fun onStart() {t }i }t
  19. 24.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> }i }t
  20. 25.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> .subscribe { viewModel -> }o }i }t
  21. 26.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i }t
  22. 27.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> when (event) {y is EditName -> }g }h .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i }t
  23. 28.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f }g }h .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i }t
  24. 29.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i }t
  25. 30.

    class ProfileViewRemix {a val navigator: Navigator val nameView: TextView val

    appService: AppService val nameSubject = BehaviorSubject.create<String>() val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i }t
  26. 31.

    val nameView: TextView val appService: AppService val nameSubject = BehaviorSubject.create<String>()

    val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i }t
  27. 32.

    val nameView: TextView val appService: AppService val nameSubject = BehaviorSubject.create<String>()

    val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i }t
  28. 33.

    val events = PublishSubject.create<Event>() fun onStart() {t events.flatMap { event

    -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i }t
  29. 34.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o
  30. 35.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y }t
  31. 36.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y }t
  32. 37.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y
  33. 38.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y
  34. 39.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y
  35. 40.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y
  36. 42.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y
  37. 43.

    fun onStart() {t events.flatMap { event -> when (event) {y

    is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) } }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )k }l })p .subscribe { viewModel -> nameView.hint = if (viewModel.prefixedName.name.isEmpty()) "Set your name" else "" nameView.text = viewModel.prefixedName.compute() // rendering other stuff }o }i fun onSetNameDialogResult(newName: String) {u events.onNext(EditName(newName)) }y
  38. 49.

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

    { override fun accept(event: Event) { }a }z View Presenter
  39. 50.

    events .subscribe(presenter) .subscribe { viewModel -> }o class ProfilePresenter :

    Consumer<Event> { override fun accept(event: Event) { }a }z View Presenter
  40. 51.

    events .subscribe(presenter) .subscribe { viewModel -> }o class ProfilePresenter :

    Consumer<Event> { override fun accept(event: Event) { event.flatMap {} }a }z View Presenter
  41. 55.
  42. 58.

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

    somefunction(events: Observable<Event>) { events.flatMap { event -> } } } View Presenter
  43. 61.

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

    viewmodels(): Observable<ViewModel> { } } View Presenter
  44. 63.

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

    override fun subscribeActual(observer: Observer<in ViewModel>) { } } View Presenter
  45. 66.

    presenter .subscribe(object : Observer<ViewModel> { override fun onComplete() {} override

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

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

    override fun subscribe(observer: Observer<in ViewModel>) { }a }z View Presenter
  47. 72.

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

    the upstream Observable and returns an ObservableSource with * optionally different element type. */ fun apply(upstream: Observable<Upstream>): ObservableSource<Downstream> }b
  48. 78.

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

    val testObserver = events // PublishSubject<Event> .compose(presenter) .test() // TestObserver<ViewModel> Test Presenter
  49. 79.

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

    val testObserver = events // PublishSubject<Event> .compose(presenter) .test() // TestObserver<ViewModel> events.onNext(EditName("Georges")) testObserver.assertValue(ViewModel()) Test Presenter
  50. 82.

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

    events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>) {k events.subscribe(this.events) }l }u View Presenter
  51. 83.

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

    events = PublishSubject.create<Event>() fun passEvents(events: Observable<Event>) {k events.subscribe(this.events) }l fun viewModels() {o return events
 .businessLogic() }i }u View Presenter
  52. 84.

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

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

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

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

    presenter.viewModels() .subscribe { viewModel -> }o
 presenter.passEvents(events) class ProfilePresenter {j

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

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

    Event() }a data class ViewModel( val stuff: Any?, val prefixedName: PrefixedName )b class ProfileViewRemix { fun onStart() {t events .compose(presenter) .subscribe { viewModel -> // rendering view model }o }i }t
  56. 95.

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

    Event() }a data class ViewModel( val stuff: Any?, val prefixedName: PrefixedName )b class ProfileViewRemix { fun onStart() {t events .compose(presenter) .subscribe { viewModel -> // rendering view model }o }i }t
  57. 96.

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

    Event() data class ChangeAvatar(val picture: File) : Event() }a data class ViewModel( val stuff: Any?, val prefixedName: PrefixedName )b class ProfileViewRemix { fun onStart() {t events .compose(presenter) .subscribe { viewModel -> // rendering view model }o }i }t
  58. 97.

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

    Event() data class ChangeAvatar(val picture: File) : Event() }a data class ViewModel( val stuff: Any?, val prefixedName: PrefixedName, val avatarUri: Uri )b class ProfileViewRemix { fun onStart() {t events .compose(presenter) .subscribe { viewModel -> // rendering view model }o }i }t
  59. 98.

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

    Event() data class ChangeAvatar(val picture: File) : Event() }a data class ViewModel( val stuff: Any?, val prefixedName: PrefixedName, val avatarUri: Uri )b class ProfileViewRemix { fun onStart() {t events .compose(presenter) .subscribe { viewModel -> // rendering view model Picasso.get().load(viewModel.avatarUri).into(avatarView) }o }i }t
  60. 99.

    class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {d return events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )f }g })h }j }k
  61. 100.

    class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {d return events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s }d }f .startWith(NameValid(event.newName)) }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )f }g })h }j }k
  62. 101.

    class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {d return events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s }d }f .startWith(NameValid(event.newName)) is ChangeAvatar -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )f }g })h }j }k
  63. 102.

    class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {d return events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s }d }f .startWith(NameValid(event.newName)) is ChangeAvatar -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } .onErrorReturn { throwable -> AvatarInvalid(throwable) } }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )f }g })h }j }k
  64. 103.

    class ProfilePresenter : ObservableTransformer<Event, ViewModel> {a override fun apply(events: Observable<Event>):

    ObservableSource<ViewModel> {d return events.flatMap { event -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s }d }f .startWith(NameValid(event.newName)) is ChangeAvatar -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } .onErrorReturn { throwable -> AvatarInvalid(throwable) } }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )f is AvatarValid -> previous.copy(avatarUri = result.uri) }g })h }j
  65. 104.

    override fun apply(events: Observable<Event>): ObservableSource<ViewModel> {d return events.flatMap { event

    -> when (event) {y is EditName -> appService.setName(event.newName) .map { response -> when {u response.isValid -> NameValid(response.name) response.isInvalid -> NameInvalid.also { navigator.goTo(response) }s }d }f .startWith(NameValid(event.newName)) is ChangeAvatar -> appService.changeAvatar(event.picture) .map { response -> AvatarValid(response.uri) } .onErrorReturn { throwable -> AvatarInvalid(throwable) } }g }h .mergeWith(appService.profile().map(::ProfileResult)) .scan(ViewModel(stuff = null, prefixedName = PrefixedName("", "")), BiFunction { previous: ViewModel, result: Result -> when (result) {j is NameValid -> previous.copy(prefixedName = previous.prefixedName.copy(name = result.name)) is NameInvalid -> previous is ProfileResult -> previous.copy( stuff = result.profile, prefixedName = PrefixedName(result.profile.prefix, result.profile.name) )f is AvatarValid -> previous.copy(avatarUri = result.uri) is AvatarInvalid -> TODO() }g })h }j
  66. 105.

    Architecture forced us to separate concerns No need to touch

    existing code Easy to test Easy to review