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

Solving all problems with reactive streams

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for pakoito 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

Avatar for pakoito

pakoito

April 20, 2018
Tweet

More Decks by pakoito

Other Decks in Technology

Transcript

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

  3. 5

  4. @pacoworks Requirements Select any number of songs Play previews inline

    Filters by genre Do not display songs on soundtrack 6
  5. @pacoworks Making data simple Everything is plain data Data values

    aggregate and change over time Provide sensible default values 8
  6. @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. @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. @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. @pacoworks Dumb passive views Receive plain data from the state

    Keep dumb copies of the state for internal use 13
  10. @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. @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. @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. @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. @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. @pacoworks Providing information Identify all information sources Different model from

    the data layer Write abstractions that can be reusable 20
  16. @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. @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. @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. @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. @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. @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. @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. @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. @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. @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. @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. @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. @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. @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. @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. @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. @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