Slide 1

Slide 1 text

@pacoworks Solving all problems with reactive streams Paco Estevez - 2018 1

Slide 2

Slide 2 text

@pacoworks Key Takeaways Take business requirements and convert them into a reactive domain model Create your own solutions to general problems Apply those solution to specific use cases Write code for maintainability 2

Slide 3

Slide 3 text

@pacoworks 3 Use case: Song Picker

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

5

Slide 6

Slide 6 text

@pacoworks Requirements Select any number of songs Play previews inline Filters by genre Do not display songs on soundtrack 6

Slide 7

Slide 7 text

@pacoworks 7 Modeling the data

Slide 8

Slide 8 text

@pacoworks Making data simple Everything is plain data Data values aggregate and change over time Provide sensible default values 8

Slide 9

Slide 9 text

@pacoworks Information breakdown Select any number of songs Play previews inline Filters by genre Do not display songs already on soundtrack 9 List of songs Set of selected songs Play information: total and currently played List of filters Currently selected filter List of soundtrack songs

Slide 10

Slide 10 text

@pacoworks Model 10 List of songs List of selected songs Play information: total and currently played List of filters Currently selected filter List of soundtrack songs typealias Genre = String data class Song(val genre: Genre, val url: String, ...) data class Beat(val current: Int, val total: Int) data class SoundtrackElement(val song: Song, ...)

Slide 11

Slide 11 text

@pacoworks State 11 Select any number of songs Play previews inline Filters by genre Do not display songs already on soundtrack val songList: BehaviorSubject> = BehaviorSubject.create(emptyList()) val songSelected: BehaviorSubject> = BehaviorSubject.create(emptySet()) val filterList: BehaviorSubject> = BehaviorSubject.create(emptyList()) val filterSelected: BehaviorSubject> = BehaviorSubject.create(emptySet()) val soundtrack: BehaviorSubject> = BehaviorSubject.create(emptyList()) Say bye bye to songs on soundtrack for now Notice there is no Beat state. Beats are transient data.

Slide 12

Slide 12 text

@pacoworks 12 Displaying the information to the user

Slide 13

Slide 13 text

@pacoworks Dumb passive views Receive plain data from the state Keep dumb copies of the state for internal use 13

Slide 14

Slide 14 text

@pacoworks View display Select any number of songs Play previews inline Filters by genre 14 interface MusicViewDisplay { fun showSongs(songs: List) fun showSelected(selected: Set) fun showBeat(beat: Tuple2) fun clearBeat() fun showGenres(genres: List) fun showVisible(visible: Set) }

Slide 15

Slide 15 text

@pacoworks Binding State to UI fun bind(lifecycle: Observable, mainThreadScheduler: Scheduler, state: Observable, viewAction: (T) -> Unit) = lifecycle .filter(is(CREATE)) .switchMap(state) .observeOn(mainThreadScheduler) .takeUntil( lifecycle .filter(is(DESTROY))) .subscribe(viewAction) 15 No leaky UI! Bind with lifecycle assurance Add main thread too Observable subscribed with (T) -> Unit

Slide 16

Slide 16 text

@pacoworks View display Select any number of songs Play previews inline Filters by genre 16 val bindSimple = ::bind.apply( lifecycle, AndroidSchedulers.mainThread()) bindSimple(songList, ::showSongs) bindSimple(songSelected, ::showSelected) bindSimple(filterList, ::showGenres) bindSimple(filterSelected, ::showVisible) Beats have no permanent state -> no binding!

Slide 17

Slide 17 text

@pacoworks View adapter Select any number of songs Play previews inline Filters by genre 17 class MusicAdapter: RecyclerView.Adapter { val songs: List val selected: Set val visible: Set val beat: AtomicReference> /*...*/ } class MusicFilterSpinner: BaseAdapter { val genres: List /*...*/ }

Slide 18

Slide 18 text

@pacoworks View adapter Select any number of songs Play previews inline Filters by genre 18 class MusicAdapter: RecyclerView.Adapter { val selected: Set val visible: Set val beat: AtomicReference> fun getItemViewType(pos: Int, element: Song): Int = if visible.contains(element) SHOW else HIDDEN fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH = if (viewType == HIDDEN) HiddenSongViewHolder(parent.context) else SongViewHolder(/*...*/) fun onBindViewHolder(holder: VH, pos: Int) { val position = songs.get(pos); if (selected.contains(position)) { /*...*/ } val beatC = beat.get() if (beatC != null && beatC.b == position) { /*...*/ } } }

Slide 19

Slide 19 text

@pacoworks 19 Interacting with services

Slide 20

Slide 20 text

@pacoworks Providing information Identify all information sources Different model from the data layer Write abstractions that can be reusable 20

Slide 21

Slide 21 text

@pacoworks Information sources Select any number of songs Play previews inline Filters by genre 21 Song Provider: network Media Player: Android Framework Filters: derived from list of songs

Slide 22

Slide 22 text

@pacoworks Song Provider Select any number of songs Play previews inline Filters by genre Do not display songs already on soundtrack 22 interface SongService { fun getListOfSongs(): Observable> } interface SongAPI { fun getSongs(): Observable> } object SongProvider: SongService { override fun getListOfSongs(): Observable> = /*...*/ }

Slide 23

Slide 23 text

@pacoworks Song Provider Select any number of songs Play previews inline Filters by genre Do not display songs already on soundtrack 23 object SongProvider: SongService { override fun getListOfSongs(): Observable> = getAPI().getSongs() // Failure policy .retry(3) .flatMap { songPOJOs -> toSongList(songPOJOs) } .flatMap { songs -> getSoundtrack().flatMap { soundtrack -> Observable.from(songs).filter { song -> !soundtrack.contains(song) } } } .toList() // Error handling with a good default .onErrorReturn(emptyList()) }

Slide 24

Slide 24 text

@pacoworks Media Player Select any number of songs Play previews inline Filters by genre 24 data class AndroidMusicPlayer(url: String) { fun play(): Observable = /* ... */ fun observeBeats(): Observable = /* ... */ fun stop(): Observable = /* ... */ } interface SongPlayer { fun playSong(url: String): Observable } data class Player(val lfcy: Observable): SongPlayer { override fun playSong(url: String): Observable = AndroidMusicPlayer(url).let { player -> player.play() .flatMap { player.observeBeats() } .takeUntil(lfcy.filter { it is PAUSE }) .concatWith(player.stop()) } }

Slide 25

Slide 25 text

@pacoworks Media Player Select any number of songs Play previews inline Filters by genre 25 data class AndroidMusicPlayer private constructor (url: String) { fun play(): Observable = /* ... */ fun observeBeats(): Observable = /* ... */ fun stop(): Observable = /* ... */ companion object { operator fun invoke(url: String) = Observable.create { /* url validation code */ } } } data class Player(val lfcy: Observable): SongPlayer { override fun playSong(url: String): Observable = AndroidMusicPlayer(url).flatMap { player -> player.play() .flatMap { player.observeBeats() } .takeUntil(lfcy.filter { it is PAUSE }) .concatWith(player.stop()) } }

Slide 26

Slide 26 text

@pacoworks User Interaction Select any number of songs Play previews inline Filters by genre 26 interface UserInteractionProvider {
 fun clickSong(): Observable>
 fun clickPlay(): Observable>
 fun clickStop(): Observable> fun clickFilter(): Observable fun clickSave(): Observable } Two separate buttons for play/stop

Slide 27

Slide 27 text

@pacoworks Writing our business logic 27

Slide 28

Slide 28 text

@pacoworks Breaking down use cases Match data sources with resulting state Work on small, self-contained ideas Use cases talk between them by modifying state State gets applied to UI automatically 28

Slide 29

Slide 29 text

@pacoworks Loading songs & filters 29 // Service fun getListOfSongs() : Observable> // State val songList : BehaviorSubject> val filterList : BehaviorSubject> songList.switchMap { currentSongs -> if (currentSongs.isEmpty()) { getListOfSongs() } else { Observable.empty() } }.doOnNext(songList) .flatMap { songs -> Observable.from(songs) .map { song -> song.genre } .distinct() .toList() }.subscribe(filterList) Beware the default value!

Slide 30

Slide 30 text

@pacoworks Selecting songs 30 // Service fun clickSong() : Observable>
 // State val songSelected : BehaviorSubject> songSelected.switchMap { selectedSongs -> clickSong().switchMap { songClicked -> if (selectedSongs.contains(songClicked)){ selectedSongs .run { remove(songClicked) } } else { selectedSongs .run { add(songClicked) } } } }.subscribe(songSelected) Add and remove return a new List

Slide 31

Slide 31 text

@pacoworks Selecting filters 31 // Service fun clickFilter() : Observable // State val songList : BehaviorSubject> val filterSelected : BehaviorSubject> filterSelected.switchMap { _ -> clickFilter().switchMap { genre -> songList.first().map { songs -> if (genre.isNotEmpty()) { songs.filter { song -> song.genre == genre } } else { songs } } } }.subscribe(filterSelected) Hot into cold observable Beware the default value!

Slide 32

Slide 32 text

@pacoworks Playing songs 32 // Services fun clickPlay() : Observable>
 fun clickStop() : Observable> fun playSong(url: String) : Observable // Display fun showBeat( beat:Tuple2) fun clearBeat() clickPlay().switchMap { songPosition -> playSong(songPosition.element.url) .map { beat -> beat to songPosition.element } .takeUntil( clickStop().filter { stopPosition -> songPosition == stopPosition }) .doOnUnsubscribe(view::clearBeat) }.subscribe(view::showBeat) No state, values are transient! Lifecycle already embedded in playSong!

Slide 33

Slide 33 text

@pacoworks Saving to soundtrack 33 // Service fun clickSave() : Observable // State val songSelected : BehaviorSubject> val soundtrack : BehaviorSubject> songSelected.switchMap { selectedSongs -> clickSave().switchMap { _ -> soundtrack.map { soundtrackSongs -> soundtrackSongs + selectedSongs.toList() } } }.subscribe(soundtrack)

Slide 34

Slide 34 text

@pacoworks Improving maintainability 34

Slide 35

Slide 35 text

@pacoworks Saving to soundtrack - with syntactic sugar 35 // Service fun clickSave() : Observable // State val songSelected : BehaviorSubject> val soundtrack : BehaviorSubject> ObservableKW.monadErrorSwitch().binding { val selectedSongs = songSelected.bind().toList() clickSave().bind() val currentSoundtrack = soundtrack.bind() currentSoundtrack + selectedSongs }.fix().subscribe(soundtrack)

Slide 36

Slide 36 text

@pacoworks Saving to soundtrack - with other frameworks 36 // Service fun clickSave() : ??? // State val songSelected : ???> val soundtrack : ???> IO.monadError().binding { /*...*/ }.fix().unsafeRunAsync { soundtrack.set(it) } PromiseK.monadError().binding { /*...*/ }.fix().success { soundtrack.set(it) } FlowableK.monadErrorSwitch().binding { /*...*/ }.fix().subscribe(soundtrack)

Slide 37

Slide 37 text

@pacoworks Saving to soundtrack - framework agnostic 37 fun MonadError.saveToSoundtrack( songSelected : Kind>, soundtrack : Kind>, clickSave: () -> Kind) = binding { val selectedSongs = songSelected.bind().toList() clickSave().bind() val currentSoundtrack = soundtrack.bind() currentSoundtrack + selectedSongs } // Service fun clickSave() : Kind // State val songSelected : Kind> val soundtrack : Kind>

Slide 38

Slide 38 text

@pacoworks Saving to soundtrack - framework agnostic 38 ObservableK.monadErrorSwitch().saveToSoundtrack(/*…*/) .fix().subscribe(soundtrack) IO.monadError().saveToSoundtrack(/*…*/) .fix().unsafeRunAsync { soundtrack.set(it) } PromiseK.monadError().saveToSoundtrack(/*…*/) .fix().success { soundtrack.set(it) } FlowableK.monadErrorSwitch().saveToSoundtrack(/*…*/) .fix().subscribe(soundtrack) // Service fun clickSave() : Kind // State val songSelected : Kind> val soundtrack : Kind> // Business logic fun MonadError.saveToSoudtrack( songSelected : Kind>, soundtrack : Kind>, clickSave: () -> Kind)

Slide 39

Slide 39 text

@pacoworks Arrow: http://arrow-kt.io/ Fully Reactive Apps: http://www.pacoworks.com/2016/11/02/fully- reactive-apps-at-droidcon-uk-2016-2/ Domain modeling with sealed classes: http://www.pacoworks.com/2016/10/03/new-talk- a-domain-driven-approach-to-kotlins-new-types- at-mobilization-2016/ DI with typeclasses: https://www.pacoworks.com/2018/02/25/simple- dependency-injection-in-kotlin-part-1/ Sample project: https://github.com/pakoito/ FunctionalAndroidReference Copyright slides 4 & 5 - CC BY 4.0 Carmen Martín Herrero pacoworks.com @pacoworks github.com/pakoito Slides: http://tinyurl.com/RxUseCaseAsos18 39