Solving all problems with reactive streams

B5ab42cbb595d2267cb2eaa050ec58c5?s=47 pakoito
April 20, 2018

Solving all problems with reactive streams

This session takes a deep dive into several real-world rewrites of large features in a popular app using exclusively reactive streams, from the view all the way to the network!

Fully Reactive Apps: https://speakerdeck.com/pakoito/fully-reactive-apps

B5ab42cbb595d2267cb2eaa050ec58c5?s=128

pakoito

April 20, 2018
Tweet

Transcript

  1. 2.

    @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
  2. 4.

    4

  3. 5.

    5

  4. 6.

    @pacoworks Requirements Select any number of songs Play previews inline

    Filters by genre Do not display songs on soundtrack 6
  5. 8.

    @pacoworks Making data simple Everything is plain data Data values

    aggregate and change over time Provide sensible default values 8
  6. 9.

    @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
  7. 10.

    @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, ...)
  8. 11.

    @pacoworks State 11 Select any number of songs Play previews

    inline Filters by genre Do not display songs already on soundtrack val songList: BehaviorSubject<List<Song>> = BehaviorSubject.create(emptyList()) val songSelected: BehaviorSubject<Set<Song>> = BehaviorSubject.create(emptySet()) val filterList: BehaviorSubject<List<Genre>> = BehaviorSubject.create(emptyList()) val filterSelected: BehaviorSubject<Set<Song>> = BehaviorSubject.create(emptySet()) val soundtrack: BehaviorSubject<List<Song>> = BehaviorSubject.create(emptyList()) Say bye bye to songs on soundtrack for now Notice there is no Beat state. Beats are transient data.
  9. 13.

    @pacoworks Dumb passive views Receive plain data from the state

    Keep dumb copies of the state for internal use 13
  10. 14.

    @pacoworks View display Select any number of songs Play previews

    inline Filters by genre 14 interface MusicViewDisplay { fun showSongs(songs: List<Song>) fun showSelected(selected: Set<Song>) fun showBeat(beat: Tuple2<Beat, Song>) fun clearBeat() fun showGenres(genres: List<Genre>) fun showVisible(visible: Set<Song>) }
  11. 15.

    @pacoworks Binding State to UI fun <T> bind(lifecycle: Observable<Lifecycle>, mainThreadScheduler:

    Scheduler, state: Observable<T>, 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<T> subscribed with (T) -> Unit
  12. 16.

    @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!
  13. 17.

    @pacoworks View adapter Select any number of songs Play previews

    inline Filters by genre 17 class MusicAdapter: RecyclerView.Adapter<VH> { val songs: List<Song> val selected: Set<Song> val visible: Set<Song> val beat: AtomicReference<Tuple2<Beat, Song>> /*...*/ } class MusicFilterSpinner: BaseAdapter { val genres: List<Genre> /*...*/ }
  14. 18.

    @pacoworks View adapter Select any number of songs Play previews

    inline Filters by genre 18 class MusicAdapter: RecyclerView.Adapter<VH> { val selected: Set<Song> val visible: Set<Song> val beat: AtomicReference<Tuple2<Beat, Song>> 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) { /*...*/ } } }
  15. 20.

    @pacoworks Providing information Identify all information sources Different model from

    the data layer Write abstractions that can be reusable 20
  16. 21.

    @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
  17. 22.

    @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<List<Song>> } interface SongAPI { fun getSongs(): Observable<List<SongPOJO>> } object SongProvider: SongService { override fun getListOfSongs(): Observable<List<Song>> = /*...*/ }
  18. 23.

    @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<List<Song>> = 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()) }
  19. 24.

    @pacoworks Media Player Select any number of songs Play previews

    inline Filters by genre 24 data class AndroidMusicPlayer(url: String) { fun play(): Observable<Unit> = /* ... */ fun observeBeats(): Observable<Beat> = /* ... */ fun stop(): Observable<Unit> = /* ... */ } interface SongPlayer { fun playSong(url: String): Observable<Beat> } data class Player(val lfcy: Observable<Lifecycle>): SongPlayer { override fun playSong(url: String): Observable<Beat> = AndroidMusicPlayer(url).let { player -> player.play() .flatMap { player.observeBeats() } .takeUntil(lfcy.filter { it is PAUSE }) .concatWith(player.stop()) } }
  20. 25.

    @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<Unit> = /* ... */ fun observeBeats(): Observable<Beat> = /* ... */ fun stop(): Observable<Unit> = /* ... */ companion object { operator fun invoke(url: String) = Observable.create { /* url validation code */ } } } data class Player(val lfcy: Observable<Lifecycle>): SongPlayer { override fun playSong(url: String): Observable<Beat> = AndroidMusicPlayer(url).flatMap { player -> player.play() .flatMap { player.observeBeats() } .takeUntil(lfcy.filter { it is PAUSE }) .concatWith(player.stop()) } }
  21. 26.

    @pacoworks User Interaction Select any number of songs Play previews

    inline Filters by genre 26 interface UserInteractionProvider {
 fun clickSong(): Observable<Position<Song>>
 fun clickPlay(): Observable<Position<Song>>
 fun clickStop(): Observable<Position<Song>> fun clickFilter(): Observable<Genre> fun clickSave(): Observable<Unit> } Two separate buttons for play/stop
  22. 28.

    @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
  23. 29.

    @pacoworks Loading songs & filters 29 // Service fun getListOfSongs()

    : Observable<List<Song>> // State val songList : BehaviorSubject<List<Song>> val filterList : BehaviorSubject<List<Genre>> 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!
  24. 30.

    @pacoworks Selecting songs 30 // Service fun clickSong() : Observable<Position<Song>>


    // State val songSelected : BehaviorSubject<Set<Song>> 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
  25. 31.

    @pacoworks Selecting filters 31 // Service fun clickFilter() : Observable<Genre>

    // State val songList : BehaviorSubject<List<Song>> val filterSelected : BehaviorSubject<Set<Song>> 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!
  26. 32.

    @pacoworks Playing songs 32 // Services fun clickPlay() : Observable<Position<Song>>


    fun clickStop() : Observable<Position<Song>> fun playSong(url: String) : Observable<Beat> // Display fun showBeat( beat:Tuple2<Beat, Song>) 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!
  27. 33.

    @pacoworks Saving to soundtrack 33 // Service fun clickSave() :

    Observable<Unit> // State val songSelected : BehaviorSubject<Set<Song>> val soundtrack : BehaviorSubject<List<Song>> songSelected.switchMap { selectedSongs -> clickSave().switchMap { _ -> soundtrack.map { soundtrackSongs -> soundtrackSongs + selectedSongs.toList() } } }.subscribe(soundtrack)
  28. 35.

    @pacoworks Saving to soundtrack - with syntactic sugar 35 //

    Service fun clickSave() : Observable<Unit> // State val songSelected : BehaviorSubject<Set<Song>> val soundtrack : BehaviorSubject<List<Song>> ObservableKW.monadErrorSwitch().binding { val selectedSongs = songSelected.bind().toList() clickSave().bind() val currentSoundtrack = soundtrack.bind() currentSoundtrack + selectedSongs }.fix().subscribe(soundtrack)
  29. 36.

    @pacoworks Saving to soundtrack - with other frameworks 36 //

    Service fun clickSave() : ???<Unit> // State val songSelected : ???<Set<Song>> val soundtrack : ???<List<Song>> IO.monadError().binding { /*...*/ }.fix().unsafeRunAsync { soundtrack.set(it) } PromiseK.monadError().binding { /*...*/ }.fix().success { soundtrack.set(it) } FlowableK.monadErrorSwitch().binding { /*...*/ }.fix().subscribe(soundtrack)
  30. 37.

    @pacoworks Saving to soundtrack - framework agnostic 37 fun <F>

    MonadError<F>.saveToSoundtrack( songSelected : Kind<F, Set<Song>>, soundtrack : Kind<F, List<Song>>, clickSave: () -> Kind<F, Unit>) = binding { val selectedSongs = songSelected.bind().toList() clickSave().bind() val currentSoundtrack = soundtrack.bind() currentSoundtrack + selectedSongs } // Service fun clickSave() : Kind<F, Unit> // State val songSelected : Kind<F, Set<Song>> val soundtrack : Kind<F, List<Song>>
  31. 38.

    @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<F, Unit> // State val songSelected : Kind<F, Set<Song>> val soundtrack : Kind<F, List<Song>> // Business logic fun <F> MonadError<F>.saveToSoudtrack( songSelected : Kind<F, Set<Song>>, soundtrack : Kind<F, List<Song>>, clickSave: () -> Kind<F, Unit>)
  32. 39.

    @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