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

OLD - Solving all problems with reactive streams

pakoito
October 05, 2017

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

pakoito

October 05, 2017
Tweet

More Decks by pakoito

Other Decks in Programming

Transcript

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

    View Slide

  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

    View Slide

  3. @pacoworks
    3
    Use case: Song Picker

    View Slide

  4. 4

    View Slide

  5. 5

    View Slide

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

    View Slide

  7. @pacoworks
    7
    Modeling the data

    View Slide

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

    View Slide

  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

    View Slide

  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, ...)

    View Slide

  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> =
    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.

    View Slide

  12. @pacoworks
    12
    Displaying the information
    to the user

    View Slide

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

    View Slide

  14. @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)
    }

    View Slide

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

    View Slide

  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!

    View Slide

  17. @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
    /*...*/
    }

    View Slide

  18. @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) { /*...*/ }
    }
    }

    View Slide

  19. @pacoworks
    19
    Interacting with services

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  24. @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(is(PAUSE))
    .doOnUnsubscribe { player.stop() }
    }
    }

    View Slide

  25. @pacoworks
    User Interaction
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    25
    interface UserInteractionProvider {

    fun clickSong(): Observable>

    fun clickPlay(): Observable>

    fun clickStop(): Observable>
    fun clickFilter(): Observable
    fun clickSave(): Observable
    }
    Two separate buttons for play/stop

    View Slide

  26. @pacoworks
    Writing our business logic
    26

    View Slide

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

    View Slide

  28. @pacoworks
    Loading songs & filters
    28
    // 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!

    View Slide

  29. @pacoworks
    Selecting songs
    29
    // Service
    fun clickSong()
    : Observable>

    // State
    val songSelected
    : BehaviorSubject>
    songSelected.switchMap { selectedSongs ->
    clickSong().switchMap { songClicked ->
    if (selectedSongs.contains(songClicked)){
    selectedSongs
    .let { remove(songClicked) }
    } else {
    selectedSongs
    .let { add(songClicked) }
    }
    }
    }.subscribe(songSelected)
    Add and remove return a new List

    View Slide

  30. @pacoworks
    Selecting filters
    30
    // 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)
    Beware the default value!

    View Slide

  31. @pacoworks
    Playing songs
    31
    // 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!

    View Slide

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

    View Slide

  33. @pacoworks
    Improving
    maintainability
    33

    View Slide

  34. @pacoworks
    Saving to soundtrack
    - with syntactic sugar
    34
    // 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()
    yields(currentSoundtrack + selectedSongs)
    }.subscribe(soundtrack)

    View Slide

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

    View Slide

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

    View Slide

  37. @pacoworks
    Saving to soundtrack
    - framework agnostic
    37
    saveToSoundtrack(/*...*/, ObservableKW.monadErrorSwitch())
    .subscribe(soundtrack)
    saveToSoundtrack(/*...*/, IO.monadError())
    .unsafeRunAsync { soundtrack.set(it) }
    saveToSoundtrack(/*...*/, PromiseKW.monadError())
    .success { soundtrack.set(it) }
    saveToSoundtrack(/*...*/, FlowableKW.monadErrorSwitch())
    .subscribe(soundtrack)
    // Service
    fun clickSave()
    : HK
    // State
    val songSelected
    : HK>
    val soundtrack
    : HK>
    // Business logic
    fun saveToSoudtrack(
    songSelected : HK>,
    soundtrack : HK>,
    clickSave: () -> HK,
    binder: MonadError)

    View Slide

  38. @pacoworks
    Kategory:
    http://kategory.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/
    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/RxUseCaseMobi17
    This presentation will be soon available on the Mobiconf
    website at the following link:
    https://2017.mobiconf.org/
    38

    View Slide